@datatruck/cli 0.22.1 → 0.23.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/Task/MysqlDumpTask.d.ts +4 -0
- package/Task/MysqlDumpTask.js +109 -86
- package/config.schema.json +4 -0
- package/package.json +3 -1
- package/utils/async.d.ts +19 -0
- package/utils/async.js +49 -0
- package/utils/fs.d.ts +1 -0
- package/utils/fs.js +17 -1
- package/utils/mysql.d.ts +23 -7
- package/utils/mysql.js +87 -33
package/Task/MysqlDumpTask.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type MysqlDumpTaskConfigType = {
|
|
|
7
7
|
*/
|
|
8
8
|
dataFormat?: "csv" | "sql";
|
|
9
9
|
csvSharedPath?: string;
|
|
10
|
+
/**
|
|
11
|
+
* @default 1
|
|
12
|
+
*/
|
|
13
|
+
concurrency?: number;
|
|
10
14
|
} & SqlDumpTaskConfigType;
|
|
11
15
|
export declare const mysqlDumpTaskDefinition: import("json-schema").JSONSchema7;
|
|
12
16
|
export declare class MysqlDumpTask extends TaskAbstract<MysqlDumpTaskConfigType> {
|
package/Task/MysqlDumpTask.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MysqlDumpTask = exports.mysqlDumpTaskDefinition = exports.mysqlDumpTaskName = void 0;
|
|
4
4
|
const AppError_1 = require("../Error/AppError");
|
|
5
|
+
const async_1 = require("../utils/async");
|
|
5
6
|
const cli_1 = require("../utils/cli");
|
|
6
7
|
const config_1 = require("../utils/datatruck/config");
|
|
7
8
|
const fs_1 = require("../utils/fs");
|
|
@@ -16,6 +17,7 @@ const path_1 = require("path");
|
|
|
16
17
|
exports.mysqlDumpTaskName = "mysql-dump";
|
|
17
18
|
exports.mysqlDumpTaskDefinition = (0, SqlDumpTaskAbstract_1.sqlDumpTaskDefinition)({
|
|
18
19
|
dataFormat: { enum: ["csv", "sql"] },
|
|
20
|
+
concurrency: { type: "integer", minimum: 1 },
|
|
19
21
|
csvSharedPath: { type: "string" },
|
|
20
22
|
});
|
|
21
23
|
const suffix = {
|
|
@@ -27,85 +29,92 @@ const suffix = {
|
|
|
27
29
|
};
|
|
28
30
|
class MysqlDumpTask extends TaskAbstract_1.TaskAbstract {
|
|
29
31
|
async onBackup(data) {
|
|
30
|
-
const sql = (0, mysql_1.createMysqlCli)({
|
|
32
|
+
const sql = await (0, mysql_1.createMysqlCli)({
|
|
31
33
|
...this.config,
|
|
32
34
|
verbose: data.options.verbose,
|
|
33
35
|
});
|
|
34
36
|
const tableNames = await sql.fetchTableNames(this.config.database, this.config.includeTables, this.config.excludeTables);
|
|
35
37
|
const outputPath = data.package.path;
|
|
36
38
|
(0, assert_1.ok)(typeof outputPath === "string");
|
|
39
|
+
const concurrency = this.config.concurrency ?? 4;
|
|
37
40
|
const dataFormat = this.config.dataFormat ?? "sql";
|
|
38
41
|
await (0, promises_1.mkdir)(outputPath, { recursive: true });
|
|
39
42
|
const sharedDir = dataFormat === "csv"
|
|
40
43
|
? await sql.initSharedDir(this.config.csvSharedPath)
|
|
41
44
|
: undefined;
|
|
42
45
|
if (this.config.oneFileByTable || sharedDir) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
await (0, async_1.runParallel)({
|
|
47
|
+
items: tableNames,
|
|
48
|
+
concurrency,
|
|
49
|
+
onChange: async ({ processed: proccesed, buffer }) => await data.onProgress({
|
|
46
50
|
relative: {
|
|
47
51
|
description: "Exporting",
|
|
48
|
-
payload:
|
|
52
|
+
payload: [...buffer.keys()].join(", "),
|
|
49
53
|
},
|
|
50
54
|
absolute: {
|
|
51
55
|
total: tableNames.length,
|
|
52
|
-
current,
|
|
53
|
-
percent: (0, math_1.progressPercent)(tableNames.length,
|
|
56
|
+
current: proccesed,
|
|
57
|
+
percent: (0, math_1.progressPercent)(tableNames.length, proccesed),
|
|
54
58
|
},
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
(
|
|
60
|
-
|
|
59
|
+
}),
|
|
60
|
+
onItem: async ({ item: tableName, index, controller }) => {
|
|
61
|
+
if (sharedDir) {
|
|
62
|
+
const tableSharedPath = (0, path_1.join)(sharedDir, `tmp-dtt-backup-${data.snapshot.id.slice(0, 8)}-${tableName}`);
|
|
63
|
+
if (data.options.verbose) {
|
|
64
|
+
(0, cli_1.logExec)("mkdir", ["-p", tableSharedPath]);
|
|
65
|
+
(0, cli_1.logExec)("chmod", ["777", tableSharedPath]);
|
|
66
|
+
}
|
|
67
|
+
await (0, promises_1.mkdir)(tableSharedPath, { recursive: true });
|
|
68
|
+
try {
|
|
69
|
+
await (0, promises_1.chmod)(tableSharedPath, 0o777);
|
|
70
|
+
await sql.csvDump({
|
|
71
|
+
sharedPath: tableSharedPath,
|
|
72
|
+
items: [tableName],
|
|
73
|
+
database: this.config.database,
|
|
74
|
+
onSpawn: (p) => (controller.stop = () => p.kill()),
|
|
75
|
+
});
|
|
76
|
+
const files = await (0, promises_1.readdir)(tableSharedPath);
|
|
77
|
+
const schemaFile = `${tableName}.sql`;
|
|
78
|
+
const dataFile = `${tableName}.txt`;
|
|
79
|
+
const successCsvDump = files.length === 2 &&
|
|
80
|
+
files.every((file) => file === schemaFile || file === dataFile);
|
|
81
|
+
if (!successCsvDump)
|
|
82
|
+
throw new AppError_1.AppError(`Invalid csv dump files: ${files.join(", ")}`);
|
|
83
|
+
await (0, fs_1.safeRename)((0, path_1.join)(tableSharedPath, schemaFile), (0, path_1.join)(outputPath, `${tableName}${suffix.tableSchema}`));
|
|
84
|
+
await (0, fs_1.safeRename)((0, path_1.join)(tableSharedPath, dataFile), (0, path_1.join)(outputPath, `${tableName}${suffix.tableData}`));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await (0, promises_1.rm)(tableSharedPath, { recursive: true });
|
|
88
|
+
}
|
|
61
89
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
sharedPath: tableSharedPath,
|
|
90
|
+
else {
|
|
91
|
+
const outPath = (0, path_1.join)(outputPath, `${tableName}${suffix.table}`);
|
|
92
|
+
await sql.dump({
|
|
93
|
+
output: outPath,
|
|
67
94
|
items: [tableName],
|
|
68
95
|
database: this.config.database,
|
|
96
|
+
onSpawn: (p) => (controller.stop = () => p.kill()),
|
|
97
|
+
...(concurrency !== 1 && {
|
|
98
|
+
onProgress(progress) {
|
|
99
|
+
data.onProgress({
|
|
100
|
+
relative: {
|
|
101
|
+
description: "Exporting",
|
|
102
|
+
payload: tableName,
|
|
103
|
+
current: progress.totalBytes,
|
|
104
|
+
format: "size",
|
|
105
|
+
},
|
|
106
|
+
absolute: {
|
|
107
|
+
total: tableNames.length,
|
|
108
|
+
current: index,
|
|
109
|
+
percent: (0, math_1.progressPercent)(tableNames.length, index),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
69
114
|
});
|
|
70
|
-
const files = await (0, promises_1.readdir)(tableSharedPath);
|
|
71
|
-
const schemaFile = `${tableName}.sql`;
|
|
72
|
-
const dataFile = `${tableName}.txt`;
|
|
73
|
-
const successCsvDump = files.length === 2 &&
|
|
74
|
-
files.every((file) => file === schemaFile || file === dataFile);
|
|
75
|
-
if (!successCsvDump)
|
|
76
|
-
throw new AppError_1.AppError(`Invalid csv dump files: ${files.join(", ")}`);
|
|
77
|
-
await (0, promises_1.rename)((0, path_1.join)(tableSharedPath, schemaFile), (0, path_1.join)(outputPath, `${tableName}${suffix.tableSchema}`));
|
|
78
|
-
await (0, promises_1.rename)((0, path_1.join)(tableSharedPath, dataFile), (0, path_1.join)(outputPath, `${tableName}${suffix.tableData}`));
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
await (0, promises_1.rm)(tableSharedPath, { recursive: true });
|
|
82
115
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const outPath = (0, path_1.join)(outputPath, `${tableName}${suffix.table}`);
|
|
86
|
-
await sql.dump({
|
|
87
|
-
output: outPath,
|
|
88
|
-
items: [tableName],
|
|
89
|
-
database: this.config.database,
|
|
90
|
-
onProgress(progress) {
|
|
91
|
-
data.onProgress({
|
|
92
|
-
relative: {
|
|
93
|
-
description: "Exporting",
|
|
94
|
-
payload: tableName,
|
|
95
|
-
current: progress.totalBytes,
|
|
96
|
-
format: "size",
|
|
97
|
-
},
|
|
98
|
-
absolute: {
|
|
99
|
-
total: tableNames.length,
|
|
100
|
-
current,
|
|
101
|
-
percent: (0, math_1.progressPercent)(tableNames.length, current),
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
current++;
|
|
108
|
-
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
109
118
|
}
|
|
110
119
|
else {
|
|
111
120
|
await data.onProgress({
|
|
@@ -136,7 +145,7 @@ class MysqlDumpTask extends TaskAbstract_1.TaskAbstract {
|
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
async onRestore(data) {
|
|
139
|
-
const sql = (0, mysql_1.createMysqlCli)({
|
|
148
|
+
const sql = await (0, mysql_1.createMysqlCli)({
|
|
140
149
|
...this.config,
|
|
141
150
|
verbose: data.options.verbose,
|
|
142
151
|
});
|
|
@@ -181,47 +190,61 @@ class MysqlDumpTask extends TaskAbstract_1.TaskAbstract {
|
|
|
181
190
|
await sql.createDatabase(database);
|
|
182
191
|
if (data.options.verbose)
|
|
183
192
|
(0, cli_1.logExec)("readdir", [restorePath]);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
const concurrency = this.config.concurrency ?? 1;
|
|
194
|
+
await (0, async_1.runParallel)({
|
|
195
|
+
items: files.filter((f) => !f.endsWith(suffix.tableData)),
|
|
196
|
+
concurrency,
|
|
197
|
+
onChange: async ({ processed: proccesed, buffer }) => await data.onProgress({
|
|
188
198
|
relative: {
|
|
189
199
|
description: "Importing",
|
|
190
|
-
payload:
|
|
200
|
+
payload: [...buffer.keys()].join(", "),
|
|
191
201
|
},
|
|
192
202
|
absolute: {
|
|
193
203
|
total: files.length,
|
|
194
|
-
current:
|
|
195
|
-
percent: (0, math_1.progressPercent)(files.length,
|
|
204
|
+
current: proccesed,
|
|
205
|
+
percent: (0, math_1.progressPercent)(files.length, proccesed),
|
|
196
206
|
},
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
}),
|
|
208
|
+
onItem: async ({ item: file, controller }) => {
|
|
209
|
+
await sql.importFile({
|
|
210
|
+
path: (0, path_1.join)(restorePath, file),
|
|
211
|
+
database: database.name,
|
|
212
|
+
onSpawn: (p) => (controller.stop = () => p.kill()),
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
await (0, async_1.runParallel)({
|
|
217
|
+
items: dataFiles,
|
|
218
|
+
concurrency,
|
|
219
|
+
onChange: async ({ processed: proccesed, buffer }) => await data.onProgress({
|
|
205
220
|
relative: {
|
|
206
221
|
description: "Importing",
|
|
207
|
-
payload:
|
|
222
|
+
payload: [...buffer.keys()].join(", "),
|
|
208
223
|
},
|
|
209
224
|
absolute: {
|
|
210
225
|
total: files.length,
|
|
211
|
-
current:
|
|
212
|
-
percent: (0, math_1.progressPercent)(files.length,
|
|
226
|
+
current: proccesed,
|
|
227
|
+
percent: (0, math_1.progressPercent)(files.length, proccesed),
|
|
213
228
|
},
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
}),
|
|
230
|
+
onItem: async ({ item: file, controller }) => {
|
|
231
|
+
const filePath = (0, path_1.join)(restorePath, file);
|
|
232
|
+
const tableName = file.slice(0, suffix.tableData.length * -1);
|
|
233
|
+
const sharedFilePath = (0, path_1.join)(sharedDir, `tmp-dtt-restore-${data.snapshot.id.slice(0, 8)}-${tableName}.data.csv`);
|
|
234
|
+
try {
|
|
235
|
+
await (0, fs_1.safeRename)(filePath, sharedFilePath);
|
|
236
|
+
await sql.importCsvFile({
|
|
237
|
+
path: sharedFilePath,
|
|
238
|
+
database: database.name,
|
|
239
|
+
table: tableName,
|
|
240
|
+
onSpawn: (p) => (controller.stop = () => p.kill()),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await (0, promises_1.rm)(sharedFilePath);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
});
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
250
|
exports.MysqlDumpTask = MysqlDumpTask;
|
package/config.schema.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datatruck/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"dependencies": {
|
|
5
|
+
"@supercharge/promise-pool": "^3.1.0",
|
|
5
6
|
"ajv": "^8.12.0",
|
|
6
7
|
"async": "^3.2.4",
|
|
7
8
|
"chalk": "^4.1.2",
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
"fast-folder-size": "^2.2.0",
|
|
12
13
|
"fast-glob": "^3.3.1",
|
|
13
14
|
"micromatch": "^4.0.5",
|
|
15
|
+
"mysql2": "^3.6.1",
|
|
14
16
|
"pretty-bytes": "^5.6.0",
|
|
15
17
|
"sqlite": "^5.0.1",
|
|
16
18
|
"sqlite3": "^5.1.6"
|
package/utils/async.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type ControllerItem = {
|
|
2
|
+
stop?: () => void;
|
|
3
|
+
};
|
|
4
|
+
type ItemBuffer<T> = Map<T, ControllerItem>;
|
|
5
|
+
export declare function runParallel<T>(options: {
|
|
6
|
+
items: T[];
|
|
7
|
+
concurrency: number;
|
|
8
|
+
onChange: (data: {
|
|
9
|
+
buffer: ItemBuffer<T>;
|
|
10
|
+
processed: number;
|
|
11
|
+
proccesing: number;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
onItem: (data: {
|
|
14
|
+
item: T;
|
|
15
|
+
index: number;
|
|
16
|
+
controller: ControllerItem;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export {};
|
package/utils/async.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runParallel = void 0;
|
|
4
|
+
const promise_pool_1 = require("@supercharge/promise-pool");
|
|
5
|
+
async function runParallel(options) {
|
|
6
|
+
const buffer = new Map();
|
|
7
|
+
let processed = 0;
|
|
8
|
+
let error;
|
|
9
|
+
await promise_pool_1.PromisePool.for(options.items)
|
|
10
|
+
.withConcurrency(options.concurrency)
|
|
11
|
+
.process(async (item, index, pool) => {
|
|
12
|
+
const controller = {};
|
|
13
|
+
buffer.set(item, controller);
|
|
14
|
+
try {
|
|
15
|
+
await options.onChange({
|
|
16
|
+
processed,
|
|
17
|
+
proccesing: buffer.size,
|
|
18
|
+
buffer,
|
|
19
|
+
});
|
|
20
|
+
await options.onItem({
|
|
21
|
+
item,
|
|
22
|
+
index,
|
|
23
|
+
controller,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (_error) {
|
|
27
|
+
error = _error;
|
|
28
|
+
buffer.delete(item);
|
|
29
|
+
pool.stop();
|
|
30
|
+
for (const [, $controller] of buffer.entries())
|
|
31
|
+
try {
|
|
32
|
+
$controller.stop?.();
|
|
33
|
+
}
|
|
34
|
+
catch (_) { }
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
buffer.delete(item);
|
|
38
|
+
processed++;
|
|
39
|
+
await options.onChange({
|
|
40
|
+
processed,
|
|
41
|
+
proccesing: buffer.size,
|
|
42
|
+
buffer,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (error)
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
exports.runParallel = runParallel;
|
package/utils/fs.d.ts
CHANGED
|
@@ -120,4 +120,5 @@ export declare function createWriteStreamPool(options: {
|
|
|
120
120
|
};
|
|
121
121
|
export declare function countFileLines(path: string): Promise<number>;
|
|
122
122
|
export declare function fetchData<T>(input: T, onPath?: (input: Exclude<T, string>) => string | undefined): Promise<string | null>;
|
|
123
|
+
export declare function safeRename(oldPath: string, newPath: string): Promise<void>;
|
|
123
124
|
export {};
|
package/utils/fs.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.fetchData = exports.countFileLines = exports.createWriteStreamPool = exports.createFileScanner = exports.cpy = exports.isNotFoundError = exports.updateFileStats = exports.copyFileWithStreams = exports.waitForClose = exports.writeGitIgnoreList = exports.fastglobToGitIgnore = exports.forEachFile = exports.readDir = exports.readPartialFile = exports.mkTmpDir = exports.fastFolderSizeAsync = exports.tmpDir = exports.rmTmpDir = exports.isTmpDir = exports.sessionTmpDir = exports.parentTmpDir = exports.findFile = exports.parsePackageFile = exports.parseFile = exports.parseFileExtensions = exports.writeJSONFile = exports.existsFile = exports.existsDir = exports.safeStat = exports.ensureEmptyDir = exports.mkdirIfNotExists = exports.isLocalDir = exports.isEmptyDir = exports.isWSLSystem = void 0;
|
|
6
|
+
exports.safeRename = exports.fetchData = exports.countFileLines = exports.createWriteStreamPool = exports.createFileScanner = exports.cpy = exports.isNotFoundError = exports.updateFileStats = exports.copyFileWithStreams = exports.waitForClose = exports.writeGitIgnoreList = exports.fastglobToGitIgnore = exports.forEachFile = exports.readDir = exports.readPartialFile = exports.mkTmpDir = exports.fastFolderSizeAsync = exports.tmpDir = exports.rmTmpDir = exports.isTmpDir = exports.sessionTmpDir = exports.parentTmpDir = exports.findFile = exports.parsePackageFile = exports.parseFile = exports.parseFileExtensions = exports.writeJSONFile = exports.existsFile = exports.existsDir = exports.safeStat = exports.ensureEmptyDir = exports.mkdirIfNotExists = exports.isLocalDir = exports.isEmptyDir = exports.isWSLSystem = void 0;
|
|
7
7
|
const globalData_1 = __importDefault(require("../globalData"));
|
|
8
8
|
const math_1 = require("./math");
|
|
9
9
|
const path_1 = require("./path");
|
|
@@ -544,3 +544,19 @@ async function fetchData(input, onPath) {
|
|
|
544
544
|
return null;
|
|
545
545
|
}
|
|
546
546
|
exports.fetchData = fetchData;
|
|
547
|
+
async function safeRename(oldPath, newPath) {
|
|
548
|
+
try {
|
|
549
|
+
await (0, promises_1.rename)(oldPath, newPath);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (error instanceof Error &&
|
|
553
|
+
error.message.includes("cross-device link not permitted")) {
|
|
554
|
+
await (0, promises_1.cp)(oldPath, newPath, { recursive: true });
|
|
555
|
+
await (0, promises_1.rm)(oldPath, { recursive: true });
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
exports.safeRename = safeRename;
|
package/utils/mysql.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChildProcess } from "child_process";
|
|
1
2
|
export type MysqlCliOptions = {
|
|
2
3
|
password: string | {
|
|
3
4
|
path: string;
|
|
@@ -6,24 +7,33 @@ export type MysqlCliOptions = {
|
|
|
6
7
|
port?: number;
|
|
7
8
|
username: string;
|
|
8
9
|
verbose?: boolean;
|
|
10
|
+
database?: string;
|
|
9
11
|
};
|
|
10
|
-
export declare function createMysqlCli(options: MysqlCliOptions): {
|
|
12
|
+
export declare function createMysqlCli(options: MysqlCliOptions): Promise<{
|
|
11
13
|
options: MysqlCliOptions;
|
|
12
14
|
initSharedDir: (sharedDir?: string) => Promise<string>;
|
|
13
15
|
args: () => Promise<string[]>;
|
|
14
|
-
run: (query: string, database?: string, extra?: string[]) => Promise<import("./process").ExecResultType>;
|
|
15
|
-
|
|
16
|
+
run: (query: string, database?: string, extra?: string[], onSpawn?: ((p: ChildProcess) => void) | undefined) => Promise<import("./process").ExecResultType>;
|
|
17
|
+
execute: (query: string, params?: any[]) => Promise<void>;
|
|
18
|
+
insert: (tableName: string, item: Record<string, any>) => Promise<void>;
|
|
19
|
+
changeDatabase: (name: string) => Promise<void>;
|
|
20
|
+
fetchAll: <T>(query: string, params?: any[]) => Promise<T[]>;
|
|
16
21
|
dump: (input: {
|
|
17
22
|
output: string;
|
|
18
23
|
database: string;
|
|
19
24
|
items?: string[] | undefined;
|
|
20
25
|
onlyStoredPrograms?: boolean | undefined;
|
|
26
|
+
onSpawn?: ((p: ChildProcess) => void) | undefined;
|
|
21
27
|
onProgress?: ((data: {
|
|
22
28
|
totalBytes: number;
|
|
23
29
|
}) => void) | undefined;
|
|
24
30
|
}) => Promise<[void, import("./process").ExecResultType]>;
|
|
25
31
|
fetchTableNames: (database: string, include?: string[], exclude?: string[]) => Promise<string[]>;
|
|
26
|
-
importFile: (
|
|
32
|
+
importFile: (input: {
|
|
33
|
+
path: string;
|
|
34
|
+
database: string;
|
|
35
|
+
onSpawn?: ((p: ChildProcess) => void) | undefined;
|
|
36
|
+
}) => Promise<import("./process").ExecResultType>;
|
|
27
37
|
isDatabaseEmpty: (database: string) => Promise<boolean>;
|
|
28
38
|
createDatabase: (database: {
|
|
29
39
|
name: string;
|
|
@@ -32,8 +42,14 @@ export declare function createMysqlCli(options: MysqlCliOptions): {
|
|
|
32
42
|
csvDump: (input: {
|
|
33
43
|
database: string;
|
|
34
44
|
sharedPath: string;
|
|
35
|
-
items?: string[];
|
|
45
|
+
items?: string[] | undefined;
|
|
46
|
+
onSpawn?: ((p: ChildProcess) => void) | undefined;
|
|
36
47
|
}) => Promise<void>;
|
|
37
|
-
importCsvFile: (
|
|
48
|
+
importCsvFile: (input: {
|
|
49
|
+
path: string;
|
|
50
|
+
database: string;
|
|
51
|
+
table: string;
|
|
52
|
+
onSpawn?: ((p: ChildProcess) => void) | undefined;
|
|
53
|
+
}) => Promise<import("./process").ExecResultType>;
|
|
38
54
|
fetchVariable: (name: string) => Promise<string | undefined>;
|
|
39
|
-
}
|
|
55
|
+
}>;
|
package/utils/mysql.js
CHANGED
|
@@ -2,19 +2,39 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createMysqlCli = void 0;
|
|
4
4
|
const AppError_1 = require("../Error/AppError");
|
|
5
|
+
const cli_1 = require("./cli");
|
|
5
6
|
const fs_1 = require("./fs");
|
|
6
7
|
const process_1 = require("./process");
|
|
7
8
|
const string_1 = require("./string");
|
|
8
9
|
const crypto_1 = require("crypto");
|
|
9
10
|
const fs_2 = require("fs");
|
|
10
11
|
const promises_1 = require("fs/promises");
|
|
12
|
+
const promise_1 = require("mysql2/promise");
|
|
11
13
|
const os_1 = require("os");
|
|
12
14
|
const path_1 = require("path");
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function flatQuery(query, params) {
|
|
16
|
+
query = query.replace(/\s{1,}/g, " ").trim();
|
|
17
|
+
let paramIndex = 0;
|
|
18
|
+
return params
|
|
19
|
+
? query.replace(/\?/g, () => {
|
|
20
|
+
const param = params[paramIndex++];
|
|
21
|
+
return param ? JSON.stringify(param) : "?";
|
|
22
|
+
})
|
|
23
|
+
: query;
|
|
24
|
+
}
|
|
25
|
+
async function createMysqlCli(options) {
|
|
26
|
+
let sqlConfigPath;
|
|
27
|
+
const password = (await (0, fs_1.fetchData)(options.password, (p) => p.path)) ?? "";
|
|
28
|
+
const sql = await (0, promise_1.createConnection)({
|
|
29
|
+
host: options.hostname,
|
|
30
|
+
user: options.username,
|
|
31
|
+
password,
|
|
32
|
+
port: options.port,
|
|
33
|
+
database: options.database,
|
|
34
|
+
});
|
|
35
|
+
async function createSqlConfig() {
|
|
36
|
+
if (sqlConfigPath)
|
|
37
|
+
return sqlConfigPath;
|
|
18
38
|
const dir = await (0, fs_1.mkTmpDir)("mysql-cli");
|
|
19
39
|
const password = await (0, fs_1.fetchData)(options.password, (p) => p.path);
|
|
20
40
|
const data = [
|
|
@@ -24,30 +44,31 @@ function createMysqlCli(options) {
|
|
|
24
44
|
`user = "${options.username}"`,
|
|
25
45
|
`password = "${password}"`,
|
|
26
46
|
];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return defaultsFilePath;
|
|
47
|
+
await (0, promises_1.writeFile)((sqlConfigPath = (0, path_1.join)(dir, "mysql.conf")), data.join("\n"));
|
|
48
|
+
return sqlConfigPath;
|
|
30
49
|
}
|
|
31
50
|
async function args() {
|
|
32
|
-
return [`--defaults-file=${await
|
|
51
|
+
return [`--defaults-file=${await createSqlConfig()}`];
|
|
33
52
|
}
|
|
34
|
-
async function run(query, database, extra = []) {
|
|
53
|
+
async function run(query, database, extra = [], onSpawn) {
|
|
35
54
|
return await (0, process_1.exec)("mysql", [
|
|
36
55
|
...(await args()),
|
|
37
56
|
...(database ? [database] : []),
|
|
38
57
|
...(extra || []),
|
|
39
58
|
"-e",
|
|
40
|
-
query
|
|
59
|
+
flatQuery(query),
|
|
41
60
|
"-N",
|
|
42
61
|
"--silent",
|
|
43
62
|
], undefined, {
|
|
63
|
+
onSpawn,
|
|
44
64
|
log: options.verbose,
|
|
45
65
|
stderr: { toExitCode: true },
|
|
46
66
|
stdout: { save: true },
|
|
47
67
|
});
|
|
48
68
|
}
|
|
49
|
-
async function fetchAll(query,
|
|
50
|
-
|
|
69
|
+
async function fetchAll(query, params) {
|
|
70
|
+
const [rows] = await sql.query(query, params);
|
|
71
|
+
return rows;
|
|
51
72
|
}
|
|
52
73
|
async function fetchTableNames(database, include, exclude) {
|
|
53
74
|
return (await fetchAll(`
|
|
@@ -56,11 +77,11 @@ function createMysqlCli(options) {
|
|
|
56
77
|
FROM
|
|
57
78
|
information_schema.tables
|
|
58
79
|
WHERE
|
|
59
|
-
table_schema =
|
|
80
|
+
table_schema = ?
|
|
60
81
|
ORDER BY
|
|
61
82
|
table_name
|
|
62
|
-
|
|
63
|
-
.map((r) => r
|
|
83
|
+
`, [database]))
|
|
84
|
+
.map((r) => r.table_name)
|
|
64
85
|
.filter((0, string_1.createMatchFilter)(include, exclude));
|
|
65
86
|
}
|
|
66
87
|
async function dump(input) {
|
|
@@ -89,6 +110,7 @@ function createMysqlCli(options) {
|
|
|
89
110
|
...(input.items || []),
|
|
90
111
|
], null, {
|
|
91
112
|
stderr: { toExitCode: true },
|
|
113
|
+
onSpawn: input.onSpawn,
|
|
92
114
|
pipe: {
|
|
93
115
|
stream,
|
|
94
116
|
onWriteProgress: input.onProgress,
|
|
@@ -107,11 +129,14 @@ function createMysqlCli(options) {
|
|
|
107
129
|
input.database,
|
|
108
130
|
"--lock-tables=false",
|
|
109
131
|
"--skip-add-drop-table=false",
|
|
132
|
+
"--fields-terminated-by=0x09",
|
|
133
|
+
"--lines-terminated-by=0x0a",
|
|
110
134
|
"-T",
|
|
111
135
|
input.sharedPath,
|
|
112
136
|
...(input.items || []),
|
|
113
137
|
], null, {
|
|
114
138
|
stderr: { toExitCode: true },
|
|
139
|
+
onSpawn: input.onSpawn,
|
|
115
140
|
log: {
|
|
116
141
|
exec: options.verbose,
|
|
117
142
|
stderr: options.verbose,
|
|
@@ -119,7 +144,7 @@ function createMysqlCli(options) {
|
|
|
119
144
|
},
|
|
120
145
|
});
|
|
121
146
|
}
|
|
122
|
-
async function importFile(
|
|
147
|
+
async function importFile(input) {
|
|
123
148
|
return await (0, process_1.exec)("mysql", [
|
|
124
149
|
...(await args()),
|
|
125
150
|
`--init-command=SET ${[
|
|
@@ -127,10 +152,11 @@ function createMysqlCli(options) {
|
|
|
127
152
|
"unique_checks=0",
|
|
128
153
|
"foreign_key_checks=0",
|
|
129
154
|
].join(",")};`,
|
|
130
|
-
database,
|
|
155
|
+
input.database,
|
|
131
156
|
], null, {
|
|
157
|
+
onSpawn: input.onSpawn,
|
|
132
158
|
pipe: {
|
|
133
|
-
stream: (0, fs_2.createReadStream)(path),
|
|
159
|
+
stream: (0, fs_2.createReadStream)(input.path),
|
|
134
160
|
onReadProgress: (data) => {
|
|
135
161
|
if (options.verbose)
|
|
136
162
|
(0, process_1.logExecStdout)({
|
|
@@ -145,35 +171,36 @@ function createMysqlCli(options) {
|
|
|
145
171
|
log: options.verbose,
|
|
146
172
|
});
|
|
147
173
|
}
|
|
148
|
-
async function importCsvFile(
|
|
174
|
+
async function importCsvFile(input) {
|
|
149
175
|
return run(`
|
|
150
|
-
LOAD DATA LOCAL INFILE
|
|
151
|
-
INTO TABLE ${table}
|
|
152
|
-
FIELDS TERMINATED BY '
|
|
153
|
-
|
|
154
|
-
LINES TERMINATED BY '\\n'`, database, ["--local-infile"]);
|
|
176
|
+
LOAD DATA LOCAL INFILE ${JSON.stringify(input.path.replaceAll("\\", "/"))}
|
|
177
|
+
INTO TABLE ${input.table}
|
|
178
|
+
FIELDS TERMINATED BY '\\t'
|
|
179
|
+
LINES TERMINATED BY '\\n'`, input.database, ["--local-infile"], input.onSpawn);
|
|
155
180
|
}
|
|
156
181
|
async function isDatabaseEmpty(database) {
|
|
157
|
-
const [
|
|
182
|
+
const [row] = await fetchAll(`
|
|
158
183
|
SELECT
|
|
159
184
|
COUNT(*) AS total
|
|
160
185
|
FROM
|
|
161
186
|
information_schema.tables
|
|
162
187
|
WHERE
|
|
163
|
-
table_schema =
|
|
164
|
-
|
|
165
|
-
return Number(total) ? false : true;
|
|
188
|
+
table_schema = ?
|
|
189
|
+
`, [database]);
|
|
190
|
+
return Number(row.total) ? false : true;
|
|
166
191
|
}
|
|
167
192
|
async function createDatabase(database) {
|
|
168
|
-
await
|
|
193
|
+
await sql.execute(`
|
|
169
194
|
CREATE DATABASE IF NOT EXISTS \`${database.name}\`
|
|
170
195
|
CHARACTER SET ${database.charset ?? "utf8"}
|
|
171
196
|
COLLATE ${database.charset ?? "utf8_general_ci"}
|
|
172
197
|
`);
|
|
173
198
|
}
|
|
174
199
|
async function fetchVariable(name) {
|
|
175
|
-
const
|
|
176
|
-
|
|
200
|
+
const rows = await fetchAll(`SHOW VARIABLES LIKE ?`, [
|
|
201
|
+
name,
|
|
202
|
+
]);
|
|
203
|
+
return (0, string_1.undefIfEmpty)(rows?.[0].Value);
|
|
177
204
|
}
|
|
178
205
|
async function initSharedDir(sharedDir) {
|
|
179
206
|
const secure_file_priv = await fetchVariable("secure_file_priv");
|
|
@@ -193,7 +220,7 @@ function createMysqlCli(options) {
|
|
|
193
220
|
try {
|
|
194
221
|
await (0, fs_1.mkdirIfNotExists)(dir);
|
|
195
222
|
await (0, promises_1.chmod)(dir, 0o777);
|
|
196
|
-
await
|
|
223
|
+
await sql.execute(`SELECT 1 INTO OUTFILE ${outFileVar}`);
|
|
197
224
|
const exists = await (0, fs_1.existsFile)(outFile);
|
|
198
225
|
if (!exists)
|
|
199
226
|
throw new AppError_1.AppError(`MySQL shared dir is not reached: ${dir}`);
|
|
@@ -205,11 +232,38 @@ function createMysqlCli(options) {
|
|
|
205
232
|
catch (e) { }
|
|
206
233
|
}
|
|
207
234
|
}
|
|
235
|
+
async function execute(query, params = []) {
|
|
236
|
+
if (options.verbose) {
|
|
237
|
+
(0, cli_1.logExec)(`mysql`, [
|
|
238
|
+
...(await args()),
|
|
239
|
+
"-e",
|
|
240
|
+
`"${flatQuery(query)}"`,
|
|
241
|
+
...(sql.config.database ? [sql.config.database] : []),
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
await sql.execute(query, params);
|
|
245
|
+
}
|
|
246
|
+
async function insert(tableName, item) {
|
|
247
|
+
const columnsExpr = Object.keys(item)
|
|
248
|
+
.map((v) => v)
|
|
249
|
+
.join(", ");
|
|
250
|
+
const paramsExpr = Array.from({ length: Object.keys(item).length })
|
|
251
|
+
.fill("?")
|
|
252
|
+
.join(", ");
|
|
253
|
+
const params = Object.values(item);
|
|
254
|
+
await execute(`INSERT INTO ${tableName} (${columnsExpr}) VALUES (${paramsExpr})`, params);
|
|
255
|
+
}
|
|
256
|
+
async function changeDatabase(name) {
|
|
257
|
+
await sql.changeUser({ database: name });
|
|
258
|
+
}
|
|
208
259
|
return {
|
|
209
260
|
options,
|
|
210
261
|
initSharedDir,
|
|
211
262
|
args,
|
|
212
263
|
run,
|
|
264
|
+
execute,
|
|
265
|
+
insert,
|
|
266
|
+
changeDatabase,
|
|
213
267
|
fetchAll,
|
|
214
268
|
dump,
|
|
215
269
|
fetchTableNames,
|