dbgate-api 6.3.2 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -7
- package/src/controllers/archive.js +99 -6
- package/src/controllers/auth.js +3 -1
- package/src/controllers/config.js +135 -22
- package/src/controllers/connections.js +35 -2
- package/src/controllers/databaseConnections.js +101 -2
- package/src/controllers/files.js +59 -0
- package/src/controllers/jsldata.js +9 -0
- package/src/controllers/runners.js +25 -5
- package/src/controllers/serverConnections.js +22 -2
- package/src/controllers/storage.js +4 -0
- package/src/controllers/uploads.js +0 -46
- package/src/currentVersion.js +2 -2
- package/src/main.js +7 -1
- package/src/proc/connectProcess.js +14 -2
- package/src/proc/databaseConnectionProcess.js +70 -5
- package/src/proc/serverConnectionProcess.js +7 -1
- package/src/proc/sessionProcess.js +15 -178
- package/src/shell/archiveReader.js +3 -1
- package/src/shell/collectorWriter.js +2 -2
- package/src/shell/copyStream.js +1 -0
- package/src/shell/dataReplicator.js +96 -0
- package/src/shell/download.js +22 -6
- package/src/shell/index.js +12 -2
- package/src/shell/jsonLinesWriter.js +4 -3
- package/src/shell/queryReader.js +10 -3
- package/src/shell/unzipDirectory.js +91 -0
- package/src/shell/unzipJsonLinesData.js +60 -0
- package/src/shell/unzipJsonLinesFile.js +59 -0
- package/src/shell/zipDirectory.js +49 -0
- package/src/shell/zipJsonLinesData.js +49 -0
- package/src/storageModel.js +819 -0
- package/src/utility/DatastoreProxy.js +4 -0
- package/src/utility/cloudUpgrade.js +1 -59
- package/src/utility/connectUtility.js +3 -1
- package/src/utility/crypting.js +137 -22
- package/src/utility/extractSingleFileFromZip.js +77 -0
- package/src/utility/getMapExport.js +2 -0
- package/src/utility/handleQueryStream.js +186 -0
- package/src/utility/healthStatus.js +12 -1
- package/src/utility/listZipEntries.js +41 -0
- package/src/utility/processArgs.js +5 -0
- package/src/utility/sshTunnel.js +13 -2
- package/src/shell/dataDuplicator.js +0 -61
|
@@ -60,6 +60,10 @@ class DatastoreProxy {
|
|
|
60
60
|
// if (this.disconnected) return;
|
|
61
61
|
this.subprocess = null;
|
|
62
62
|
});
|
|
63
|
+
this.subprocess.on('error', err => {
|
|
64
|
+
logger.error(extractErrorLogData(err), 'Error in data store subprocess');
|
|
65
|
+
this.subprocess = null;
|
|
66
|
+
});
|
|
63
67
|
this.subprocess.send({ msgtype: 'open', file: this.file });
|
|
64
68
|
}
|
|
65
69
|
return this.subprocess;
|
|
@@ -1,61 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const fsp = require('fs/promises');
|
|
4
|
-
const semver = require('semver');
|
|
5
|
-
const currentVersion = require('../currentVersion');
|
|
6
|
-
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
|
7
|
-
|
|
8
|
-
const logger = getLogger('cloudUpgrade');
|
|
9
|
-
|
|
10
|
-
async function checkCloudUpgrade() {
|
|
11
|
-
try {
|
|
12
|
-
const resp = await axios.default.get('https://api.github.com/repos/dbgate/dbgate/releases/latest');
|
|
13
|
-
const json = resp.data;
|
|
14
|
-
const version = json.name.substring(1);
|
|
15
|
-
let cloudDownloadedVersion = null;
|
|
16
|
-
try {
|
|
17
|
-
cloudDownloadedVersion = await fsp.readFile(process.env.CLOUD_UPGRADE_FILE + '.version', 'utf-8');
|
|
18
|
-
} catch (err) {
|
|
19
|
-
cloudDownloadedVersion = null;
|
|
20
|
-
}
|
|
21
|
-
if (
|
|
22
|
-
semver.gt(version, currentVersion.version) &&
|
|
23
|
-
(!cloudDownloadedVersion || semver.gt(version, cloudDownloadedVersion))
|
|
24
|
-
) {
|
|
25
|
-
logger.info(`New version available: ${version}`);
|
|
26
|
-
const zipUrl = json.assets.find(x => x.name == 'cloud-build.zip').browser_download_url;
|
|
27
|
-
|
|
28
|
-
const writer = fs.createWriteStream(process.env.CLOUD_UPGRADE_FILE);
|
|
29
|
-
|
|
30
|
-
const response = await axios.default({
|
|
31
|
-
url: zipUrl,
|
|
32
|
-
method: 'GET',
|
|
33
|
-
responseType: 'stream',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
response.data.pipe(writer);
|
|
37
|
-
|
|
38
|
-
await new Promise((resolve, reject) => {
|
|
39
|
-
writer.on('finish', resolve);
|
|
40
|
-
writer.on('error', reject);
|
|
41
|
-
});
|
|
42
|
-
await fsp.writeFile(process.env.CLOUD_UPGRADE_FILE + '.version', version);
|
|
43
|
-
|
|
44
|
-
logger.info(`Downloaded new version from ${zipUrl}`);
|
|
45
|
-
} else {
|
|
46
|
-
logger.info(`Checked version ${version} is not newer than ${cloudDownloadedVersion ?? currentVersion.version}, upgrade skippped`);
|
|
47
|
-
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
logger.error(extractErrorLogData(err), 'Error checking cloud upgrade');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function startCloudUpgradeTimer() {
|
|
54
|
-
// at first in 5 seconds
|
|
55
|
-
setTimeout(checkCloudUpgrade, 5000);
|
|
56
|
-
|
|
57
|
-
// hourly
|
|
58
|
-
setInterval(checkCloudUpgrade, 60 * 60 * 1000);
|
|
59
|
-
}
|
|
1
|
+
function startCloudUpgradeTimer() {}
|
|
60
2
|
|
|
61
3
|
module.exports = startCloudUpgradeTimer;
|
|
@@ -96,7 +96,9 @@ async function connectUtility(driver, storedConnection, connectionMode, addition
|
|
|
96
96
|
...decryptConnection(connectionLoaded),
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
if (!connection.port && driver.defaultPort)
|
|
99
|
+
if (!connection.port && driver.defaultPort) {
|
|
100
|
+
connection.port = driver.defaultPort.toString();
|
|
101
|
+
}
|
|
100
102
|
|
|
101
103
|
if (connection.useSshTunnel) {
|
|
102
104
|
const tunnel = await getSshTunnelProxy(connection);
|
package/src/utility/crypting.js
CHANGED
|
@@ -5,12 +5,16 @@ const path = require('path');
|
|
|
5
5
|
const _ = require('lodash');
|
|
6
6
|
|
|
7
7
|
const { datadir } = require('./directories');
|
|
8
|
+
const { encryptionKeyArg } = require('./processArgs');
|
|
8
9
|
|
|
9
10
|
const defaultEncryptionKey = 'mQAUaXhavRGJDxDTXSCg7Ej0xMmGCrx6OKA07DIMBiDcYYkvkaXjTAzPUEHEHEf9';
|
|
10
11
|
|
|
11
12
|
let _encryptionKey = null;
|
|
12
13
|
|
|
13
14
|
function loadEncryptionKey() {
|
|
15
|
+
if (encryptionKeyArg) {
|
|
16
|
+
return encryptionKeyArg;
|
|
17
|
+
}
|
|
14
18
|
if (_encryptionKey) {
|
|
15
19
|
return _encryptionKey;
|
|
16
20
|
}
|
|
@@ -33,9 +37,29 @@ function loadEncryptionKey() {
|
|
|
33
37
|
return _encryptionKey;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
async function loadEncryptionKeyFromExternal(storedValue, setStoredValue) {
|
|
41
|
+
const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
|
|
42
|
+
|
|
43
|
+
if (!storedValue) {
|
|
44
|
+
const generatedKey = crypto.randomBytes(32);
|
|
45
|
+
const newKey = generatedKey.toString('hex');
|
|
46
|
+
const result = {
|
|
47
|
+
encryptionKey: newKey,
|
|
48
|
+
};
|
|
49
|
+
await setStoredValue(encryptor.encrypt(result));
|
|
50
|
+
|
|
51
|
+
setEncryptionKey(newKey);
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = encryptor.decrypt(storedValue);
|
|
57
|
+
setEncryptionKey(data['encryptionKey']);
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
let _encryptor = null;
|
|
37
61
|
|
|
38
|
-
function
|
|
62
|
+
function getInternalEncryptor() {
|
|
39
63
|
if (_encryptor) {
|
|
40
64
|
return _encryptor;
|
|
41
65
|
}
|
|
@@ -43,35 +67,46 @@ function getEncryptor() {
|
|
|
43
67
|
return _encryptor;
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
function encryptPasswordString(password) {
|
|
71
|
+
if (password && !password.startsWith('crypt:')) {
|
|
72
|
+
return 'crypt:' + getInternalEncryptor().encrypt(password);
|
|
73
|
+
}
|
|
74
|
+
return password;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decryptPasswordString(password) {
|
|
78
|
+
if (password && password.startsWith('crypt:')) {
|
|
79
|
+
return getInternalEncryptor().decrypt(password.substring('crypt:'.length));
|
|
80
|
+
}
|
|
81
|
+
return password;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function encryptObjectPasswordField(obj, field) {
|
|
85
|
+
if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
|
|
53
86
|
return {
|
|
54
|
-
...
|
|
55
|
-
[field]: 'crypt:' +
|
|
87
|
+
...obj,
|
|
88
|
+
[field]: 'crypt:' + getInternalEncryptor().encrypt(obj[field]),
|
|
56
89
|
};
|
|
57
90
|
}
|
|
58
|
-
return
|
|
91
|
+
return obj;
|
|
59
92
|
}
|
|
60
93
|
|
|
61
|
-
function
|
|
62
|
-
if (
|
|
94
|
+
function decryptObjectPasswordField(obj, field) {
|
|
95
|
+
if (obj && obj[field] && obj[field].startsWith('crypt:')) {
|
|
63
96
|
return {
|
|
64
|
-
...
|
|
65
|
-
[field]:
|
|
97
|
+
...obj,
|
|
98
|
+
[field]: getInternalEncryptor().decrypt(obj[field].substring('crypt:'.length)),
|
|
66
99
|
};
|
|
67
100
|
}
|
|
68
|
-
return
|
|
101
|
+
return obj;
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
function encryptConnection(connection) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
105
|
+
if (connection.passwordMode != 'saveRaw') {
|
|
106
|
+
connection = encryptObjectPasswordField(connection, 'password');
|
|
107
|
+
connection = encryptObjectPasswordField(connection, 'sshPassword');
|
|
108
|
+
connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
|
|
109
|
+
}
|
|
75
110
|
return connection;
|
|
76
111
|
}
|
|
77
112
|
|
|
@@ -81,12 +116,24 @@ function maskConnection(connection) {
|
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
function decryptConnection(connection) {
|
|
84
|
-
connection =
|
|
85
|
-
connection =
|
|
86
|
-
connection =
|
|
119
|
+
connection = decryptObjectPasswordField(connection, 'password');
|
|
120
|
+
connection = decryptObjectPasswordField(connection, 'sshPassword');
|
|
121
|
+
connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
|
|
87
122
|
return connection;
|
|
88
123
|
}
|
|
89
124
|
|
|
125
|
+
function encryptUser(user) {
|
|
126
|
+
if (user.encryptPassword) {
|
|
127
|
+
user = encryptObjectPasswordField(user, 'password');
|
|
128
|
+
}
|
|
129
|
+
return user;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function decryptUser(user) {
|
|
133
|
+
user = decryptObjectPasswordField(user, 'password');
|
|
134
|
+
return user;
|
|
135
|
+
}
|
|
136
|
+
|
|
90
137
|
function pickSafeConnectionInfo(connection) {
|
|
91
138
|
if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
|
|
92
139
|
return connection;
|
|
@@ -99,10 +146,78 @@ function pickSafeConnectionInfo(connection) {
|
|
|
99
146
|
});
|
|
100
147
|
}
|
|
101
148
|
|
|
149
|
+
function setEncryptionKey(encryptionKey) {
|
|
150
|
+
_encryptionKey = encryptionKey;
|
|
151
|
+
_encryptor = null;
|
|
152
|
+
global.ENCRYPTION_KEY = encryptionKey;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getEncryptionKey() {
|
|
156
|
+
return _encryptionKey;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function generateTransportEncryptionKey() {
|
|
160
|
+
const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
|
|
161
|
+
const result = {
|
|
162
|
+
encryptionKey: crypto.randomBytes(32).toString('hex'),
|
|
163
|
+
};
|
|
164
|
+
return encryptor.encrypt(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createTransportEncryptor(encryptionData) {
|
|
168
|
+
const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
|
|
169
|
+
const data = encryptor.decrypt(encryptionData);
|
|
170
|
+
const res = simpleEncryptor.createEncryptor(data['encryptionKey']);
|
|
171
|
+
return res;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function recryptObjectPasswordField(obj, field, decryptEncryptor, encryptEncryptor) {
|
|
175
|
+
if (obj && obj[field] && obj[field].startsWith('crypt:')) {
|
|
176
|
+
return {
|
|
177
|
+
...obj,
|
|
178
|
+
[field]: 'crypt:' + encryptEncryptor.encrypt(decryptEncryptor.decrypt(obj[field].substring('crypt:'.length))),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return obj;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function recryptObjectPasswordFieldInPlace(obj, field, decryptEncryptor, encryptEncryptor) {
|
|
185
|
+
if (obj && obj[field] && obj[field].startsWith('crypt:')) {
|
|
186
|
+
obj[field] = 'crypt:' + encryptEncryptor.encrypt(decryptEncryptor.decrypt(obj[field].substring('crypt:'.length)));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function recryptConnection(connection, decryptEncryptor, encryptEncryptor) {
|
|
191
|
+
connection = recryptObjectPasswordField(connection, 'password', decryptEncryptor, encryptEncryptor);
|
|
192
|
+
connection = recryptObjectPasswordField(connection, 'sshPassword', decryptEncryptor, encryptEncryptor);
|
|
193
|
+
connection = recryptObjectPasswordField(connection, 'sshKeyfilePassword', decryptEncryptor, encryptEncryptor);
|
|
194
|
+
return connection;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function recryptUser(user, decryptEncryptor, encryptEncryptor) {
|
|
198
|
+
user = recryptObjectPasswordField(user, 'password', decryptEncryptor, encryptEncryptor);
|
|
199
|
+
return user;
|
|
200
|
+
}
|
|
201
|
+
|
|
102
202
|
module.exports = {
|
|
103
203
|
loadEncryptionKey,
|
|
104
204
|
encryptConnection,
|
|
205
|
+
encryptUser,
|
|
206
|
+
decryptUser,
|
|
105
207
|
decryptConnection,
|
|
106
208
|
maskConnection,
|
|
107
209
|
pickSafeConnectionInfo,
|
|
210
|
+
loadEncryptionKeyFromExternal,
|
|
211
|
+
getEncryptionKey,
|
|
212
|
+
setEncryptionKey,
|
|
213
|
+
encryptPasswordString,
|
|
214
|
+
decryptPasswordString,
|
|
215
|
+
|
|
216
|
+
getInternalEncryptor,
|
|
217
|
+
recryptConnection,
|
|
218
|
+
recryptUser,
|
|
219
|
+
generateTransportEncryptionKey,
|
|
220
|
+
createTransportEncryptor,
|
|
221
|
+
recryptObjectPasswordField,
|
|
222
|
+
recryptObjectPasswordFieldInPlace,
|
|
108
223
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const yauzl = require('yauzl');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
|
4
|
+
const logger = getLogger('extractSingleFileFromZip');
|
|
5
|
+
/**
|
|
6
|
+
* Extracts a single file from a ZIP using yauzl.
|
|
7
|
+
* Stops reading the rest of the archive once the file is found.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} zipPath - Path to the ZIP file on disk.
|
|
10
|
+
* @param {string} fileInZip - The file path *inside* the ZIP to extract.
|
|
11
|
+
* @param {string} outputPath - Where to write the extracted file on disk.
|
|
12
|
+
* @returns {Promise<boolean>} - Resolves with a success message or a "not found" message.
|
|
13
|
+
*/
|
|
14
|
+
function extractSingleFileFromZip(zipPath, fileInZip, outputPath) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipFile) => {
|
|
17
|
+
if (err) return reject(err);
|
|
18
|
+
|
|
19
|
+
let fileFound = false;
|
|
20
|
+
|
|
21
|
+
// Start reading the first entry
|
|
22
|
+
zipFile.readEntry();
|
|
23
|
+
|
|
24
|
+
zipFile.on('entry', entry => {
|
|
25
|
+
// Compare the entry name to the file we want
|
|
26
|
+
if (entry.fileName === fileInZip) {
|
|
27
|
+
fileFound = true;
|
|
28
|
+
|
|
29
|
+
// Open a read stream for this entry
|
|
30
|
+
zipFile.openReadStream(entry, (err, readStream) => {
|
|
31
|
+
if (err) return reject(err);
|
|
32
|
+
|
|
33
|
+
// Create a write stream to outputPath
|
|
34
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
35
|
+
readStream.pipe(writeStream);
|
|
36
|
+
|
|
37
|
+
// When the read stream ends, we can close the zipFile
|
|
38
|
+
readStream.on('end', () => {
|
|
39
|
+
// We won't read further entries
|
|
40
|
+
zipFile.close();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// When the file is finished writing, resolve
|
|
44
|
+
writeStream.on('finish', () => {
|
|
45
|
+
logger.info(`File "${fileInZip}" extracted to "${outputPath}".`);
|
|
46
|
+
resolve(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Handle write errors
|
|
50
|
+
writeStream.on('error', writeErr => {
|
|
51
|
+
logger.error(extractErrorLogData(writeErr), `Error extracting "${fileInZip}" from "${zipPath}".`);
|
|
52
|
+
reject(writeErr);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
// Not the file we want; skip to the next entry
|
|
57
|
+
zipFile.readEntry();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// If we reach the end without finding the file
|
|
62
|
+
zipFile.on('end', () => {
|
|
63
|
+
if (!fileFound) {
|
|
64
|
+
resolve(false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Handle general errors
|
|
69
|
+
zipFile.on('error', err => {
|
|
70
|
+
logger.error(extractErrorLogData(err), `ZIP file error in ${zipPath}.`);
|
|
71
|
+
reject(err);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = extractSingleFileFromZip;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
|
|
6
|
+
const { jsldir } = require('../utility/directories');
|
|
7
|
+
|
|
8
|
+
class QueryStreamTableWriter {
|
|
9
|
+
constructor(sesid = undefined) {
|
|
10
|
+
this.currentRowCount = 0;
|
|
11
|
+
this.currentChangeIndex = 1;
|
|
12
|
+
this.initializedFile = false;
|
|
13
|
+
this.sesid = sesid;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
initializeFromQuery(structure, resultIndex) {
|
|
17
|
+
this.jslid = crypto.randomUUID();
|
|
18
|
+
this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
this.currentFile,
|
|
21
|
+
JSON.stringify({
|
|
22
|
+
...structure,
|
|
23
|
+
__isStreamHeader: true,
|
|
24
|
+
}) + '\n'
|
|
25
|
+
);
|
|
26
|
+
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
|
|
27
|
+
this.writeCurrentStats(false, false);
|
|
28
|
+
this.resultIndex = resultIndex;
|
|
29
|
+
this.initializedFile = true;
|
|
30
|
+
process.send({ msgtype: 'recordset', jslid: this.jslid, resultIndex, sesid: this.sesid });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
initializeFromReader(jslid) {
|
|
34
|
+
this.jslid = jslid;
|
|
35
|
+
this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
|
|
36
|
+
this.writeCurrentStats(false, false);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
row(row) {
|
|
40
|
+
// console.log('ACCEPT ROW', row);
|
|
41
|
+
this.currentStream.write(JSON.stringify(row) + '\n');
|
|
42
|
+
this.currentRowCount += 1;
|
|
43
|
+
|
|
44
|
+
if (!this.plannedStats) {
|
|
45
|
+
this.plannedStats = true;
|
|
46
|
+
process.nextTick(() => {
|
|
47
|
+
if (this.currentStream) this.currentStream.uncork();
|
|
48
|
+
process.nextTick(() => this.writeCurrentStats(false, true));
|
|
49
|
+
this.plannedStats = false;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
rowFromReader(row) {
|
|
55
|
+
if (!this.initializedFile) {
|
|
56
|
+
process.send({ msgtype: 'initializeFile', jslid: this.jslid, sesid: this.sesid });
|
|
57
|
+
this.initializedFile = true;
|
|
58
|
+
|
|
59
|
+
fs.writeFileSync(this.currentFile, JSON.stringify(row) + '\n');
|
|
60
|
+
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
|
|
61
|
+
this.writeCurrentStats(false, false);
|
|
62
|
+
this.initializedFile = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.row(row);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
writeCurrentStats(isFinished = false, emitEvent = false) {
|
|
70
|
+
const stats = {
|
|
71
|
+
rowCount: this.currentRowCount,
|
|
72
|
+
changeIndex: this.currentChangeIndex,
|
|
73
|
+
isFinished,
|
|
74
|
+
jslid: this.jslid,
|
|
75
|
+
};
|
|
76
|
+
fs.writeFileSync(`${this.currentFile}.stats`, JSON.stringify(stats));
|
|
77
|
+
this.currentChangeIndex += 1;
|
|
78
|
+
if (emitEvent) {
|
|
79
|
+
process.send({ msgtype: 'stats', sesid: this.sesid, ...stats });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
close(afterClose) {
|
|
84
|
+
if (this.currentStream) {
|
|
85
|
+
this.currentStream.end(() => {
|
|
86
|
+
this.writeCurrentStats(true, true);
|
|
87
|
+
if (afterClose) afterClose();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class StreamHandler {
|
|
94
|
+
constructor(queryStreamInfoHolder, resolve, startLine, sesid = undefined) {
|
|
95
|
+
this.recordset = this.recordset.bind(this);
|
|
96
|
+
this.startLine = startLine;
|
|
97
|
+
this.sesid = sesid;
|
|
98
|
+
this.row = this.row.bind(this);
|
|
99
|
+
// this.error = this.error.bind(this);
|
|
100
|
+
this.done = this.done.bind(this);
|
|
101
|
+
this.info = this.info.bind(this);
|
|
102
|
+
|
|
103
|
+
// use this for cancelling - not implemented
|
|
104
|
+
// this.stream = null;
|
|
105
|
+
|
|
106
|
+
this.plannedStats = false;
|
|
107
|
+
this.queryStreamInfoHolder = queryStreamInfoHolder;
|
|
108
|
+
this.resolve = resolve;
|
|
109
|
+
// currentHandlers = [...currentHandlers, this];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
closeCurrentWriter() {
|
|
113
|
+
if (this.currentWriter) {
|
|
114
|
+
this.currentWriter.close();
|
|
115
|
+
this.currentWriter = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
recordset(columns) {
|
|
120
|
+
this.closeCurrentWriter();
|
|
121
|
+
this.currentWriter = new QueryStreamTableWriter(this.sesid);
|
|
122
|
+
this.currentWriter.initializeFromQuery(
|
|
123
|
+
Array.isArray(columns) ? { columns } : columns,
|
|
124
|
+
this.queryStreamInfoHolder.resultIndex
|
|
125
|
+
);
|
|
126
|
+
this.queryStreamInfoHolder.resultIndex += 1;
|
|
127
|
+
|
|
128
|
+
// this.writeCurrentStats();
|
|
129
|
+
|
|
130
|
+
// this.onRow = _.throttle((jslid) => {
|
|
131
|
+
// if (jslid == this.jslid) {
|
|
132
|
+
// this.writeCurrentStats(false, true);
|
|
133
|
+
// }
|
|
134
|
+
// }, 500);
|
|
135
|
+
}
|
|
136
|
+
row(row) {
|
|
137
|
+
if (this.currentWriter) this.currentWriter.row(row);
|
|
138
|
+
else if (row.message) process.send({ msgtype: 'info', info: { message: row.message }, sesid: this.sesid });
|
|
139
|
+
// this.onRow(this.jslid);
|
|
140
|
+
}
|
|
141
|
+
// error(error) {
|
|
142
|
+
// process.send({ msgtype: 'error', error });
|
|
143
|
+
// }
|
|
144
|
+
done(result) {
|
|
145
|
+
this.closeCurrentWriter();
|
|
146
|
+
// currentHandlers = currentHandlers.filter((x) => x != this);
|
|
147
|
+
this.resolve();
|
|
148
|
+
}
|
|
149
|
+
info(info) {
|
|
150
|
+
if (info && info.line != null) {
|
|
151
|
+
info = {
|
|
152
|
+
...info,
|
|
153
|
+
line: this.startLine + info.line,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (info.severity == 'error') {
|
|
157
|
+
this.queryStreamInfoHolder.canceled = true;
|
|
158
|
+
}
|
|
159
|
+
process.send({ msgtype: 'info', info, sesid: this.sesid });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid = undefined) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const start = sqlItem.trimStart || sqlItem.start;
|
|
166
|
+
const handler = new StreamHandler(queryStreamInfoHolder, resolve, start && start.line, sesid);
|
|
167
|
+
driver.stream(dbhan, sqlItem.text, handler);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function allowExecuteCustomScript(storedConnection, driver) {
|
|
172
|
+
if (driver.readOnlySessions) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (storedConnection.isReadOnly) {
|
|
176
|
+
return false;
|
|
177
|
+
// throw new Error('Connection is read only');
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
handleQueryStream,
|
|
184
|
+
QueryStreamTableWriter,
|
|
185
|
+
allowExecuteCustomScript,
|
|
186
|
+
};
|
|
@@ -24,4 +24,15 @@ async function getHealthStatus() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
async function getHealthStatusSprinx() {
|
|
28
|
+
return {
|
|
29
|
+
overallStatus: 'OK',
|
|
30
|
+
timeStamp: new Date().toISOString(),
|
|
31
|
+
timeStampUnix: Math.floor(Date.now() / 1000),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
getHealthStatus,
|
|
37
|
+
getHealthStatusSprinx,
|
|
38
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const yauzl = require('yauzl');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lists the files in a ZIP archive using yauzl,
|
|
6
|
+
* returning an array of { fileName, uncompressedSize } objects.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} zipPath - The path to the ZIP file.
|
|
9
|
+
* @returns {Promise<Array<{fileName: string, uncompressedSize: number}>>}
|
|
10
|
+
*/
|
|
11
|
+
function listZipEntries(zipPath) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
|
14
|
+
if (err) return reject(err);
|
|
15
|
+
|
|
16
|
+
const entries = [];
|
|
17
|
+
|
|
18
|
+
// Start reading entries
|
|
19
|
+
zipfile.readEntry();
|
|
20
|
+
|
|
21
|
+
// Handle each entry
|
|
22
|
+
zipfile.on('entry', entry => {
|
|
23
|
+
entries.push({
|
|
24
|
+
fileName: entry.fileName,
|
|
25
|
+
uncompressedSize: entry.uncompressedSize,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Move on to the next entry (we’re only listing, not reading file data)
|
|
29
|
+
zipfile.readEntry();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Finished reading all entries
|
|
33
|
+
zipfile.on('end', () => resolve(entries));
|
|
34
|
+
|
|
35
|
+
// Handle errors
|
|
36
|
+
zipfile.on('error', err => reject(err));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = listZipEntries;
|
|
@@ -17,6 +17,7 @@ const processDisplayName = getNamedArg('--process-display-name');
|
|
|
17
17
|
const listenApi = process.argv.includes('--listen-api');
|
|
18
18
|
const listenApiChild = process.argv.includes('--listen-api-child') || listenApi;
|
|
19
19
|
const runE2eTests = process.argv.includes('--run-e2e-tests');
|
|
20
|
+
const encryptionKeyArg = getNamedArg('--encryption-key');
|
|
20
21
|
|
|
21
22
|
function getPassArgs() {
|
|
22
23
|
const res = [];
|
|
@@ -31,6 +32,9 @@ function getPassArgs() {
|
|
|
31
32
|
if (runE2eTests) {
|
|
32
33
|
res.push('--run-e2e-tests');
|
|
33
34
|
}
|
|
35
|
+
if (global['ENCRYPTION_KEY']) {
|
|
36
|
+
res.push('--encryption-key', global['ENCRYPTION_KEY']);
|
|
37
|
+
}
|
|
34
38
|
return res;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -45,4 +49,5 @@ module.exports = {
|
|
|
45
49
|
listenApiChild,
|
|
46
50
|
processDisplayName,
|
|
47
51
|
runE2eTests,
|
|
52
|
+
encryptionKeyArg,
|
|
48
53
|
};
|
package/src/utility/sshTunnel.js
CHANGED
|
@@ -57,10 +57,21 @@ function callForwardProcess(connection, tunnelConfig, tunnelCacheKey) {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
subprocess.on('exit', code => {
|
|
60
|
-
logger.info(
|
|
60
|
+
logger.info(`SSH forward process exited with code ${code}`);
|
|
61
61
|
delete sshTunnelCache[tunnelCacheKey];
|
|
62
62
|
if (!promiseHandled) {
|
|
63
|
-
reject(
|
|
63
|
+
reject(
|
|
64
|
+
new Error(
|
|
65
|
+
'SSH forward process exited, try to change "Local host address for SSH connections" in Settings/Connections'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
subprocess.on('error', error => {
|
|
71
|
+
logger.error(extractErrorLogData(error), 'SSH forward process error');
|
|
72
|
+
delete sshTunnelCache[tunnelCacheKey];
|
|
73
|
+
if (!promiseHandled) {
|
|
74
|
+
reject(error);
|
|
64
75
|
}
|
|
65
76
|
});
|
|
66
77
|
});
|