mirror of https://github.com/Fabio286/antares.git
feat: initial mysql import support
This commit is contained in:
parent
d25c62b4da
commit
4e9f8d16ee
|
@ -31,6 +31,7 @@ module.exports = {
|
|||
schedulerAdd: true,
|
||||
schemaEdit: true,
|
||||
schemaExport: true,
|
||||
schemaImport: true,
|
||||
tableSettings: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
|
|
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
functionAdd: true,
|
||||
databaseEdit: false,
|
||||
schemaExport: true,
|
||||
schemaImport: false,
|
||||
tableSettings: true,
|
||||
viewSettings: true,
|
||||
triggerSettings: true,
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { Duplex } from 'stream';
|
||||
|
||||
export default class SqlParser extends Duplex {
|
||||
constructor (opts) {
|
||||
opts = {
|
||||
delimiter: ';',
|
||||
encoding: 'utf8',
|
||||
writableObjectMode: true,
|
||||
readableObjectMode: true,
|
||||
...opts
|
||||
};
|
||||
super(opts);
|
||||
this._buffer = [];
|
||||
this.encoding = opts.encoding;
|
||||
this.delimiter = opts.delimiter;
|
||||
|
||||
this.isEscape = false;
|
||||
this.currentQuote = '';
|
||||
this.isDelimiter = false;
|
||||
}
|
||||
|
||||
_write (chunk, encoding, next) {
|
||||
const str = chunk.toString(this.encoding);
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const currentChar = str[i];
|
||||
this.checkEscape();
|
||||
this._buffer.push(currentChar);
|
||||
// this.checkNewDelimiter(currentChar);
|
||||
this.checkQuote(currentChar);
|
||||
const query = this.getQuery();
|
||||
|
||||
if (query)
|
||||
this.push(query);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
checkEscape () {
|
||||
if (this._buffer.length > 0) {
|
||||
this.isEscape = this._buffer[this._buffer.length - 1] === '\\'
|
||||
? !this.isEscape
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
checkNewDelimiter (char) {
|
||||
if (this.parsedStr.toLowerCase() === 'delimiter' && this.currentQuote === '') {
|
||||
this.isDelimiter = true;
|
||||
this._buffer = [];
|
||||
}
|
||||
else {
|
||||
const isNewLine = ['\n', '\r'].includes(char);
|
||||
if (isNewLine && this.isDelimiter) {
|
||||
this.isDelimiter = false;
|
||||
this.delimiter = this.parsedStr;
|
||||
this._buffer = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkQuote (char) {
|
||||
const isQuote = !this.isEscape && ['"', '\''].includes(char);
|
||||
if (isQuote && this.currentQuote === char)
|
||||
this.currentQuote = '';
|
||||
|
||||
else if (isQuote && this.currentQuote === '')
|
||||
this.currentQuote = char;
|
||||
}
|
||||
|
||||
getQuery () {
|
||||
if (this.isDelimiter)
|
||||
return false;
|
||||
|
||||
let query = false;
|
||||
let demiliterFound = false;
|
||||
if (this.currentQuote === '' && this._buffer.length >= this.delimiter.length)
|
||||
demiliterFound = this.parsedStr.slice(-this.delimiter.length) === this.delimiter;
|
||||
|
||||
if (demiliterFound) {
|
||||
this._buffer.splice(-this.delimiter.length, this.delimiter.length);
|
||||
query = this.parsedStr;
|
||||
this._buffer = [];
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
get parsedStr () {
|
||||
return this._buffer.join('').trim();
|
||||
}
|
||||
|
||||
_read (size) {
|
||||
|
||||
}
|
||||
}
|
|
@ -2,10 +2,13 @@ import { ipcMain, dialog, Notification } from 'electron';
|
|||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// @TODO: need some factories
|
||||
import MysqlExporter from '../libs/exporters/sql/MysqlExporter';
|
||||
import MysqlImporter from '../libs/importers/sql/MysqlImporter';
|
||||
|
||||
export default connections => {
|
||||
let exporter = null;
|
||||
let importer = null;
|
||||
|
||||
ipcMain.handle('create-schema', async (event, params) => {
|
||||
try {
|
||||
|
@ -263,4 +266,78 @@ export default connections => {
|
|||
|
||||
return { status: 'success', response: { willAbort } };
|
||||
});
|
||||
|
||||
ipcMain.handle('import-sql', async (event, options) => {
|
||||
if (importer !== null) return;
|
||||
|
||||
switch (options.type) {
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
importer = new MysqlImporter(connections[options.uid], options);
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
status: 'error',
|
||||
response: `${type} importer not aviable`
|
||||
};
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
importer.once('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
importer.once('end', () => {
|
||||
resolve({ cancelled: importer.isCancelled });
|
||||
});
|
||||
|
||||
importer.on('progress', state => {
|
||||
event.sender.send('import-progress', state);
|
||||
});
|
||||
|
||||
importer.run();
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.cancelled) {
|
||||
new Notification({
|
||||
title: 'Import finished',
|
||||
body: `Finished importing ${path.basename(options.file)}`
|
||||
}).show();
|
||||
}
|
||||
return { status: 'success', response };
|
||||
})
|
||||
.catch(err => {
|
||||
new Notification({
|
||||
title: 'Import error',
|
||||
body: err.toString()
|
||||
}).show();
|
||||
|
||||
return { status: 'error', response: err.toString() };
|
||||
})
|
||||
.finally(() => {
|
||||
importer.removeAllListeners();
|
||||
importer = null;
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('abort-import-sql', async event => {
|
||||
let willAbort = false;
|
||||
|
||||
if (importer) {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: 'Are you sure you want to abort the import',
|
||||
buttons: ['Cancel', 'Abort'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (result.response === 1) {
|
||||
willAbort = true;
|
||||
importer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'success', response: { willAbort } };
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import fs from 'fs';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
export class BaseImporter extends EventEmitter {
|
||||
constructor (options) {
|
||||
super();
|
||||
this._options = options;
|
||||
this._isCancelled = false;
|
||||
this._fileHandler = fs.createReadStream(this._options.file, {
|
||||
flags: 'r'
|
||||
});
|
||||
this._state = {};
|
||||
|
||||
this._fileHandler.once('error', err => {
|
||||
this._isCancelled = true;
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
async run () {
|
||||
try {
|
||||
this.emit('start', this);
|
||||
await this.import();
|
||||
}
|
||||
catch (err) {
|
||||
this.emit('error', err);
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
this._fileHandler.close();
|
||||
this.emit('end');
|
||||
}
|
||||
}
|
||||
|
||||
get isCancelled () {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this._isCancelled = true;
|
||||
this.emit('cancel');
|
||||
this.emitUpdate({ op: 'cancelling' });
|
||||
}
|
||||
|
||||
emitUpdate (state) {
|
||||
this.emit('progress', { ...this._state, ...state });
|
||||
}
|
||||
|
||||
import () {
|
||||
throw new Error('Exporter must implement the "import" method');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import fs from 'fs/promises';
|
||||
import SqlParser from '../../../../common/libs/sqlParser';
|
||||
import { BaseImporter } from '../BaseImporter';
|
||||
|
||||
export default class MysqlImporter extends BaseImporter {
|
||||
constructor (client, options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
async import () {
|
||||
const { size: totalFileSize } = await fs.stat(this._options.file);
|
||||
const parser = new SqlParser();
|
||||
let readPosition = 0;
|
||||
let queryCount = 0;
|
||||
|
||||
this.emitUpdate({
|
||||
fileSize: totalFileSize,
|
||||
readPosition: 0,
|
||||
percentage: 0
|
||||
});
|
||||
|
||||
// 1. detect file encoding
|
||||
// 2. set fh encoding
|
||||
// 3. detect sql mode
|
||||
// 4. restore sql mode in case of exception
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fileHandler.pipe(parser);
|
||||
|
||||
parser.on('error', (err) => {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
parser.on('finish', () => {
|
||||
console.log('TOTAL QUERIES', queryCount);
|
||||
console.log('import end');
|
||||
resolve();
|
||||
});
|
||||
|
||||
parser.on('data', (q) => {
|
||||
console.log('query: ', q);
|
||||
queryCount++;
|
||||
});
|
||||
|
||||
this._fileHandler.on('data', (chunk) => {
|
||||
readPosition += chunk.length;
|
||||
this.emitUpdate({
|
||||
readPosition,
|
||||
percentage: readPosition / totalFileSize * 100
|
||||
});
|
||||
});
|
||||
|
||||
this._fileHandler.on('error', (e) => {
|
||||
console.log(e);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<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-up mr-1" />
|
||||
<span class="cut-text">{{ $t('message.importSchema') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
{{ sqlFile }}
|
||||
</div>
|
||||
<div class="modal-footer columns">
|
||||
<div class="column col modal-progress-wrapper text-left">
|
||||
<div class="import-progress">
|
||||
<span class="progress-status">
|
||||
{{ progressPercentage }}% - {{ progressStatus }}
|
||||
</span>
|
||||
<progress
|
||||
class="progress d-block"
|
||||
:value="progressPercentage"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-auto px-0">
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ $t('word.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
|
||||
export default {
|
||||
name: 'ModalImportSchema',
|
||||
|
||||
props: {
|
||||
selectedSchema: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
sqlFile: '',
|
||||
isImporting: false,
|
||||
progressPercentage: 0,
|
||||
progressStatus: 'Reading'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
currentWorkspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
ipcRenderer.on('import-progress', this.updateProgress);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
ipcRenderer.off('import-progress', this.updateProgress);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
refreshSchema: 'workspaces/refreshSchema'
|
||||
}),
|
||||
async startImport (sqlFile) {
|
||||
this.isImporting = true;
|
||||
this.sqlFile = sqlFile;
|
||||
|
||||
const { uid, client } = this.currentWorkspace;
|
||||
const params = {
|
||||
uid,
|
||||
type: client,
|
||||
file: sqlFile
|
||||
};
|
||||
|
||||
const result = await Schema.import(params);
|
||||
console.log(result);
|
||||
|
||||
this.isImporting = false;
|
||||
},
|
||||
updateProgress (event, state) {
|
||||
this.progressPercentage = Number(state.percentage).toFixed(1);
|
||||
},
|
||||
async closeModal () {
|
||||
let willClose = true;
|
||||
if (this.isImporting) {
|
||||
willClose = false;
|
||||
const { response } = await Schema.abortImport();
|
||||
willClose = response.willAbort;
|
||||
}
|
||||
|
||||
if (willClose)
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
|
||||
.modal-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
font-style: italic;
|
||||
font-size: 80%;
|
||||
}
|
||||
</style>
|
|
@ -65,6 +65,13 @@
|
|||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ $t('word.export') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="workspace.customizations.schemaImport"
|
||||
class="context-element"
|
||||
@click="initImport"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-up text-light pr-1" /> {{ $t('word.import') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="workspace.customizations.schemaEdit"
|
||||
class="context-element"
|
||||
|
@ -103,6 +110,12 @@
|
|||
:selected-schema="selectedSchema"
|
||||
@close="hideExportSchemaModal"
|
||||
/>
|
||||
<ModalImportSchema
|
||||
v-if="isImportSchemaModal"
|
||||
ref="importModalRef"
|
||||
:selected-schema="selectedSchema"
|
||||
@close="hideImportSchemaModal"
|
||||
/>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
|
@ -112,7 +125,9 @@ import BaseContextMenu from '@/components/BaseContextMenu';
|
|||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import ModalEditSchema from '@/components/ModalEditSchema';
|
||||
import ModalExportSchema from '@/components/ModalExportSchema';
|
||||
import ModalImportSchema from '@/components/ModalImportSchema';
|
||||
import Schema from '@/ipc-api/Schema';
|
||||
import Application from '@/ipc-api/Application';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceExploreBarSchemaContext',
|
||||
|
@ -120,7 +135,8 @@ export default {
|
|||
BaseContextMenu,
|
||||
ConfirmModal,
|
||||
ModalEditSchema,
|
||||
ModalExportSchema
|
||||
ModalExportSchema,
|
||||
ModalImportSchema
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
|
@ -130,7 +146,8 @@ export default {
|
|||
return {
|
||||
isDeleteModal: false,
|
||||
isEditModal: false,
|
||||
isExportSchemaModal: false
|
||||
isExportSchemaModal: false,
|
||||
isImportSchemaModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -187,6 +204,22 @@ export default {
|
|||
hideExportSchemaModal () {
|
||||
this.isExportSchemaModal = false;
|
||||
},
|
||||
showImportSchemaModal () {
|
||||
this.isImportSchemaModal = true;
|
||||
},
|
||||
hideImportSchemaModal () {
|
||||
this.isImportSchemaModal = false;
|
||||
},
|
||||
async initImport () {
|
||||
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql'] }] });
|
||||
if (result && !result.canceled) {
|
||||
const file = result.filePaths[0];
|
||||
this.showImportSchemaModal();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.importModalRef.startImport(file);
|
||||
});
|
||||
}
|
||||
},
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
|
|
|
@ -80,6 +80,7 @@ module.exports = {
|
|||
deterministic: 'Deterministic',
|
||||
context: 'Context',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
returns: 'Returns',
|
||||
timing: 'Timing',
|
||||
state: 'State',
|
||||
|
@ -259,6 +260,7 @@ module.exports = {
|
|||
killProcess: 'Kill process',
|
||||
closeTab: 'Close tab',
|
||||
exportSchema: 'Export schema',
|
||||
importSchema: 'Import schema',
|
||||
directoryPath: 'Directory path',
|
||||
newInserStmtEvery: 'New INSERT statement every',
|
||||
processingTableExport: 'Processing {table}',
|
||||
|
|
|
@ -80,6 +80,7 @@ module.exports = {
|
|||
deterministic: 'Deterministico',
|
||||
context: 'Contesto',
|
||||
export: 'Esporta',
|
||||
import: 'Importa',
|
||||
returns: 'Ritorna',
|
||||
timing: 'Temporizzazione',
|
||||
state: 'Stato',
|
||||
|
@ -246,6 +247,7 @@ module.exports = {
|
|||
noSchema: 'Nessuno schema',
|
||||
restorePreviourSession: 'Ripristina sessione precedente',
|
||||
exportSchema: 'Esporta schema',
|
||||
importSchema: 'Importa schema',
|
||||
directoryPath: 'Percorso directory',
|
||||
newInserStmtEvery: 'Nuova istruzione INSERT ogni',
|
||||
processingTableExport: 'Processo {table}',
|
||||
|
|
|
@ -61,4 +61,12 @@ export default class {
|
|||
static abortExport () {
|
||||
return ipcRenderer.invoke('abort-export');
|
||||
}
|
||||
|
||||
static import (params) {
|
||||
return ipcRenderer.invoke('import-sql', params);
|
||||
}
|
||||
|
||||
static abortImport () {
|
||||
return ipcRenderer.invoke('abort-import-sql');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue