Skip to content

Commit

Permalink
feat: initial db export implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
toriphes committed Oct 29, 2021
1 parent 3679121 commit 0de2321
Show file tree
Hide file tree
Showing 15 changed files with 833 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/common/customizations/mysql.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
functionAdd: true,
schedulerAdd: true,
schemaEdit: true,
schemaExport: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
Expand Down
1 change: 1 addition & 0 deletions src/common/customizations/postgresql.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
routineAdd: true,
functionAdd: true,
databaseEdit: false,
schemaExport: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
Expand Down
10 changes: 9 additions & 1 deletion src/main/ipc-handlers/application.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, ipcMain } from 'electron';
import { app, ipcMain, dialog } from 'electron';

export default () => {
ipcMain.on('close-app', () => {
Expand All @@ -9,4 +9,12 @@ export default () => {
const key = false;
event.returnValue = key;
});

ipcMain.handle('showOpenDialog', (event, options) => {
return dialog.showOpenDialog(options);
});

ipcMain.handle('get-download-dir-path', () => {
return app.getPath('downloads');
});
};
116 changes: 112 additions & 4 deletions src/main/ipc-handlers/schema.js
Original file line number Diff line number Diff line change
@@ -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 => {
let exporter = null;

ipcMain.handle('create-schema', async (event, params) => {
try {
await connections[params.uid].createSchema(params);
Expand Down Expand Up @@ -37,9 +42,16 @@ export default connections => {

ipcMain.handle('get-schema-collation', async (event, params) => {
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) {
return { status: 'error', response: err.toString() };
Expand All @@ -48,7 +60,9 @@ export default connections => {

ipcMain.handle('get-structure', async (event, params) => {
try {
const structure = await connections[params.uid].getStructure(params.schemas);
const structure = await connections[params.uid].getStructure(
params.schemas
);

return { status: 'success', response: structure };
}
Expand Down Expand Up @@ -152,4 +166,98 @@ export default connections => {
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 changes: 73 additions & 0 deletions src/main/libs/exporters/BaseExporter.js
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions src/main/libs/exporters/ExporterFactory.js
Original file line number Diff line number Diff line change
@@ -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 changes: 108 additions & 0 deletions src/main/libs/exporters/sql/MysqlExporter.js
Original file line number Diff line number Diff line change
@@ -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, '\'\'')}'`;
}
}
Loading

0 comments on commit 0de2321

Please sign in to comment.