@datatruck/cli 0.21.0 → 0.22.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/Action/BackupAction.d.ts +2 -3
- package/Action/BackupAction.js +16 -20
- package/Action/RestoreAction.d.ts +3 -1
- package/Action/RestoreAction.js +5 -5
- package/Command/BackupCommand.d.ts +1 -1
- package/Command/BackupCommand.js +1 -1
- package/Command/RestoreCommand.d.ts +1 -1
- package/Command/RestoreCommand.js +1 -1
- package/Error/AppError.d.ts +1 -0
- package/Error/AppError.js +8 -0
- package/JsonSchema/JsonSchema.js +1 -1
- package/Repository/DatatruckRepository.d.ts +3 -0
- package/Repository/DatatruckRepository.js +30 -12
- package/Task/MysqlDumpTask.d.ts +13 -15
- package/Task/MysqlDumpTask.js +207 -149
- package/Task/PostgresqlDumpTask.js +1 -1
- package/Task/SqlDumpTaskAbstract.d.ts +5 -2
- package/Task/SqlDumpTaskAbstract.js +4 -2
- package/config.schema.json +78 -4
- package/package.json +1 -1
- package/utils/cli.js +3 -1
- package/utils/datatruck/config.d.ts +1 -1
- package/utils/fs.d.ts +4 -1
- package/utils/fs.js +21 -6
- package/utils/mysql.d.ts +39 -0
- package/utils/mysql.js +211 -0
- package/utils/string.d.ts +5 -1
- package/utils/string.js +28 -6
- package/utils/tar.js +38 -33
package/utils/mysql.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type MysqlCliOptions = {
|
|
2
|
+
password: string | {
|
|
3
|
+
path: string;
|
|
4
|
+
};
|
|
5
|
+
hostname: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
username: string;
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function createMysqlCli(options: MysqlCliOptions): {
|
|
11
|
+
options: MysqlCliOptions;
|
|
12
|
+
initSharedDir: (sharedDir?: string) => Promise<string>;
|
|
13
|
+
args: () => Promise<string[]>;
|
|
14
|
+
run: (query: string, database?: string) => Promise<import("./process").ExecResultType>;
|
|
15
|
+
fetchAll: (query: string, database?: string) => Promise<string[][]>;
|
|
16
|
+
dump: (input: {
|
|
17
|
+
output: string;
|
|
18
|
+
database: string;
|
|
19
|
+
items?: string[] | undefined;
|
|
20
|
+
onlyStoredPrograms?: boolean | undefined;
|
|
21
|
+
onProgress?: ((data: {
|
|
22
|
+
totalBytes: number;
|
|
23
|
+
}) => void) | undefined;
|
|
24
|
+
}) => Promise<[void, import("./process").ExecResultType]>;
|
|
25
|
+
fetchTableNames: (database: string, include?: string[], exclude?: string[]) => Promise<string[]>;
|
|
26
|
+
importFile: (path: string, database: string) => Promise<import("./process").ExecResultType>;
|
|
27
|
+
isDatabaseEmpty: (database: string) => Promise<boolean>;
|
|
28
|
+
createDatabase: (database: {
|
|
29
|
+
name: string;
|
|
30
|
+
charset?: string;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
csvDump: (input: {
|
|
33
|
+
database: string;
|
|
34
|
+
sharedPath: string;
|
|
35
|
+
items?: string[];
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
importCsvFile: (path: string, database: string, table: string) => Promise<import("./process").ExecResultType>;
|
|
38
|
+
fetchVariable: (name: string) => Promise<string | undefined>;
|
|
39
|
+
};
|
package/utils/mysql.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMysqlCli = void 0;
|
|
4
|
+
const AppError_1 = require("../Error/AppError");
|
|
5
|
+
const fs_1 = require("./fs");
|
|
6
|
+
const process_1 = require("./process");
|
|
7
|
+
const string_1 = require("./string");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const fs_2 = require("fs");
|
|
10
|
+
const promises_1 = require("fs/promises");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
function createMysqlCli(options) {
|
|
14
|
+
async function args() {
|
|
15
|
+
const password = await (0, fs_1.fetchData)(options.password, (p) => p.path);
|
|
16
|
+
return [
|
|
17
|
+
`--host=${options.hostname}`,
|
|
18
|
+
...(options.port ? [`--port=${options.port}`] : []),
|
|
19
|
+
`--user=${options.username}`,
|
|
20
|
+
`--password=${password ?? ""}`,
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
async function run(query, database) {
|
|
24
|
+
return await (0, process_1.exec)("mysql", [
|
|
25
|
+
...(await args()),
|
|
26
|
+
...(database ? [database] : []),
|
|
27
|
+
"-e",
|
|
28
|
+
query.replace(/\s{1,}/g, " "),
|
|
29
|
+
"-N",
|
|
30
|
+
"--silent",
|
|
31
|
+
], undefined, {
|
|
32
|
+
log: options.verbose,
|
|
33
|
+
stderr: { toExitCode: true },
|
|
34
|
+
stdout: { save: true },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function fetchAll(query, database) {
|
|
38
|
+
return (0, string_1.splitLines)((await run(query, database)).stdout).map((line) => line.split("\t"));
|
|
39
|
+
}
|
|
40
|
+
async function fetchTableNames(database, include, exclude) {
|
|
41
|
+
return (await fetchAll(`
|
|
42
|
+
SELECT
|
|
43
|
+
table_name
|
|
44
|
+
FROM
|
|
45
|
+
information_schema.tables
|
|
46
|
+
WHERE
|
|
47
|
+
table_schema = '${database}'
|
|
48
|
+
ORDER BY
|
|
49
|
+
table_name
|
|
50
|
+
`))
|
|
51
|
+
.map((r) => r[0])
|
|
52
|
+
.filter((0, string_1.createMatchFilter)(include, exclude));
|
|
53
|
+
}
|
|
54
|
+
async function dump(input) {
|
|
55
|
+
const stream = (0, fs_2.createWriteStream)(input.output);
|
|
56
|
+
return await Promise.all([
|
|
57
|
+
new Promise((resolve, reject) => {
|
|
58
|
+
stream.on("close", resolve);
|
|
59
|
+
stream.on("error", reject);
|
|
60
|
+
}),
|
|
61
|
+
await (0, process_1.exec)("mysqldump", [
|
|
62
|
+
...(await args()),
|
|
63
|
+
input.database,
|
|
64
|
+
"--lock-tables=false",
|
|
65
|
+
"--skip-add-drop-table=false",
|
|
66
|
+
...(input.onlyStoredPrograms
|
|
67
|
+
? [
|
|
68
|
+
"--routines",
|
|
69
|
+
"--events",
|
|
70
|
+
"--skip-triggers",
|
|
71
|
+
"--no-create-info",
|
|
72
|
+
"--no-data",
|
|
73
|
+
"--no-create-db",
|
|
74
|
+
"--skip-opt",
|
|
75
|
+
]
|
|
76
|
+
: []),
|
|
77
|
+
...(input.items || []),
|
|
78
|
+
], null, {
|
|
79
|
+
stderr: { toExitCode: true },
|
|
80
|
+
pipe: {
|
|
81
|
+
stream,
|
|
82
|
+
onWriteProgress: input.onProgress,
|
|
83
|
+
},
|
|
84
|
+
log: {
|
|
85
|
+
exec: options.verbose,
|
|
86
|
+
stderr: options.verbose,
|
|
87
|
+
allToStderr: true,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
async function csvDump(input) {
|
|
93
|
+
await (0, process_1.exec)("mysqldump", [
|
|
94
|
+
...(await args()),
|
|
95
|
+
input.database,
|
|
96
|
+
"--lock-tables=false",
|
|
97
|
+
"--skip-add-drop-table=false",
|
|
98
|
+
"-T",
|
|
99
|
+
input.sharedPath,
|
|
100
|
+
...(input.items || []),
|
|
101
|
+
], null, {
|
|
102
|
+
stderr: { toExitCode: true },
|
|
103
|
+
log: {
|
|
104
|
+
exec: options.verbose,
|
|
105
|
+
stderr: options.verbose,
|
|
106
|
+
allToStderr: true,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function importFile(path, database) {
|
|
111
|
+
return await (0, process_1.exec)("mysql", [
|
|
112
|
+
`--init-command=SET ${[
|
|
113
|
+
"autocommit=0",
|
|
114
|
+
"unique_checks=0",
|
|
115
|
+
"foreign_key_checks=0",
|
|
116
|
+
].join(",")};`,
|
|
117
|
+
...(await args()),
|
|
118
|
+
database,
|
|
119
|
+
], null, {
|
|
120
|
+
pipe: {
|
|
121
|
+
stream: (0, fs_2.createReadStream)(path),
|
|
122
|
+
onReadProgress: (data) => {
|
|
123
|
+
if (options.verbose)
|
|
124
|
+
(0, process_1.logExecStdout)({
|
|
125
|
+
data: JSON.stringify(data),
|
|
126
|
+
colorize: true,
|
|
127
|
+
stderr: true,
|
|
128
|
+
lineSalt: true,
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
stderr: { toExitCode: true },
|
|
133
|
+
log: options.verbose,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function importCsvFile(path, database, table) {
|
|
137
|
+
return run(`
|
|
138
|
+
LOAD DATA LOCAL INFILE '${path.replaceAll("\\", "/")}'
|
|
139
|
+
INTO TABLE ${table}
|
|
140
|
+
FIELDS TERMINATED BY ','
|
|
141
|
+
ENCLOSED BY '"'
|
|
142
|
+
LINES TERMINATED BY '\\n'`, database);
|
|
143
|
+
}
|
|
144
|
+
async function isDatabaseEmpty(database) {
|
|
145
|
+
const [total] = await fetchAll(`
|
|
146
|
+
SELECT
|
|
147
|
+
COUNT(*) AS total
|
|
148
|
+
FROM
|
|
149
|
+
information_schema.tables
|
|
150
|
+
WHERE
|
|
151
|
+
table_schema = '${database}'
|
|
152
|
+
`);
|
|
153
|
+
return Number(total) ? false : true;
|
|
154
|
+
}
|
|
155
|
+
async function createDatabase(database) {
|
|
156
|
+
await run(`
|
|
157
|
+
CREATE DATABASE IF NOT EXISTS \`${database.name}\`
|
|
158
|
+
CHARACTER SET ${database.charset ?? "utf8"}
|
|
159
|
+
COLLATE ${database.charset ?? "utf8_general_ci"}
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
async function fetchVariable(name) {
|
|
163
|
+
const stdout = (0, string_1.undefIfEmpty)((await run(`SHOW VARIABLES LIKE "${name}"`)).stdout.trim());
|
|
164
|
+
return stdout ? (0, string_1.undefIfEmpty)(stdout.slice(name.length).trim()) : undefined;
|
|
165
|
+
}
|
|
166
|
+
async function initSharedDir(sharedDir) {
|
|
167
|
+
const secure_file_priv = await fetchVariable("secure_file_priv");
|
|
168
|
+
if (secure_file_priv?.toUpperCase() === "NULL")
|
|
169
|
+
throw new AppError_1.AppError("'secure_file_priv' is null in MySQL Server");
|
|
170
|
+
const dir = sharedDir ??
|
|
171
|
+
secure_file_priv ??
|
|
172
|
+
(await fetchVariable("tmpdir")) ??
|
|
173
|
+
(0, os_1.tmpdir)();
|
|
174
|
+
await checkSharedDir(dir);
|
|
175
|
+
return dir;
|
|
176
|
+
}
|
|
177
|
+
async function checkSharedDir(dir) {
|
|
178
|
+
const id = (0, crypto_1.randomBytes)(8).toString("hex");
|
|
179
|
+
const outFile = (0, path_1.join)(dir, `dtt_test_${id}`);
|
|
180
|
+
const outFileVar = JSON.stringify(outFile.replaceAll("\\", "/"));
|
|
181
|
+
try {
|
|
182
|
+
await (0, fs_1.mkdirIfNotExists)(dir);
|
|
183
|
+
await run(`SELECT 1 INTO OUTFILE ${outFileVar}`);
|
|
184
|
+
const exists = await (0, fs_1.existsFile)(outFile);
|
|
185
|
+
if (!exists)
|
|
186
|
+
throw new AppError_1.AppError(`MySQL shared dir is not reached: ${dir}`);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
try {
|
|
190
|
+
await (0, promises_1.rm)(outFile);
|
|
191
|
+
}
|
|
192
|
+
catch (e) { }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
options,
|
|
197
|
+
initSharedDir,
|
|
198
|
+
args,
|
|
199
|
+
run,
|
|
200
|
+
fetchAll,
|
|
201
|
+
dump,
|
|
202
|
+
fetchTableNames,
|
|
203
|
+
importFile,
|
|
204
|
+
isDatabaseEmpty,
|
|
205
|
+
createDatabase,
|
|
206
|
+
csvDump,
|
|
207
|
+
importCsvFile,
|
|
208
|
+
fetchVariable,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
exports.createMysqlCli = createMysqlCli;
|
package/utils/string.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export type UriType = {
|
|
|
13
13
|
export declare function formatUri(input: UriType, hidePassword?: boolean): string;
|
|
14
14
|
export declare function formatSeconds(seconds: number): string;
|
|
15
15
|
export declare function makePathPatterns(values: string[] | undefined): string[] | undefined;
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function match(path: string, include?: string[], exclude?: string[]): boolean;
|
|
17
|
+
export declare function endsWith(input: string, patterns: string[]): boolean;
|
|
18
|
+
export declare function createMatchFilter(include?: string[], exclude?: string[]): (input: string) => boolean;
|
|
17
19
|
export declare function checkMatch(subject: string | undefined, patterns: string[]): boolean;
|
|
18
20
|
export declare function formatDateTime(datetime: string): string;
|
|
21
|
+
export declare function splitLines(input: string, satinize?: boolean): string[];
|
|
22
|
+
export declare function undefIfEmpty(input: string): string | undefined;
|
package/utils/string.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatDateTime = exports.checkMatch = exports.
|
|
3
|
+
exports.undefIfEmpty = exports.splitLines = exports.formatDateTime = exports.checkMatch = exports.createMatchFilter = exports.endsWith = exports.match = exports.makePathPatterns = exports.formatSeconds = exports.formatUri = exports.parseStringList = exports.render = exports.snakeCase = exports.serialize = void 0;
|
|
4
4
|
const AppError_1 = require("../Error/AppError");
|
|
5
5
|
const micromatch_1 = require("micromatch");
|
|
6
6
|
function serialize(message, data) {
|
|
@@ -91,13 +91,19 @@ function makePathPatterns(values) {
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
exports.makePathPatterns = makePathPatterns;
|
|
94
|
-
function
|
|
95
|
-
return ((0, micromatch_1.isMatch)(path, include, {
|
|
96
|
-
dot: true,
|
|
97
|
-
}) &&
|
|
94
|
+
function match(path, include, exclude) {
|
|
95
|
+
return ((!include || (0, micromatch_1.isMatch)(path, include, { dot: true })) &&
|
|
98
96
|
(!exclude || !(0, micromatch_1.isMatch)(path, exclude, { dot: true })));
|
|
99
97
|
}
|
|
100
|
-
exports.
|
|
98
|
+
exports.match = match;
|
|
99
|
+
function endsWith(input, patterns) {
|
|
100
|
+
return patterns.some((pattern) => input.endsWith(pattern));
|
|
101
|
+
}
|
|
102
|
+
exports.endsWith = endsWith;
|
|
103
|
+
function createMatchFilter(include, exclude) {
|
|
104
|
+
return (input) => match(input, include, exclude);
|
|
105
|
+
}
|
|
106
|
+
exports.createMatchFilter = createMatchFilter;
|
|
101
107
|
function checkMatch(subject, patterns) {
|
|
102
108
|
if (!subject?.length)
|
|
103
109
|
subject = "<empty>";
|
|
@@ -114,3 +120,19 @@ function formatDateTime(datetime) {
|
|
|
114
120
|
return result;
|
|
115
121
|
}
|
|
116
122
|
exports.formatDateTime = formatDateTime;
|
|
123
|
+
function splitLines(input, satinize = true) {
|
|
124
|
+
const lines = input.split(/\r?\n/);
|
|
125
|
+
return satinize
|
|
126
|
+
? input.split(/\r?\n/).reduce((result, value) => {
|
|
127
|
+
value = value.trim();
|
|
128
|
+
if (value.length)
|
|
129
|
+
result.push(value);
|
|
130
|
+
return result;
|
|
131
|
+
}, [])
|
|
132
|
+
: lines;
|
|
133
|
+
}
|
|
134
|
+
exports.splitLines = splitLines;
|
|
135
|
+
function undefIfEmpty(input) {
|
|
136
|
+
return input.length ? input : undefined;
|
|
137
|
+
}
|
|
138
|
+
exports.undefIfEmpty = undefIfEmpty;
|
package/utils/tar.js
CHANGED
|
@@ -137,21 +137,23 @@ async function createTar(options) {
|
|
|
137
137
|
}, {
|
|
138
138
|
log: options.verbose ? { envNames: Object.keys(env) } : false,
|
|
139
139
|
stderr: { toExitCode: true },
|
|
140
|
-
stdout:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
stdout: options.onEntry
|
|
141
|
+
? {
|
|
142
|
+
parseLines: "skip-empty",
|
|
143
|
+
onData: (line) => {
|
|
144
|
+
current++;
|
|
145
|
+
const path = vendor === "bsdtar" ? line.slice(2) : line;
|
|
146
|
+
options.onEntry?.({
|
|
147
|
+
path,
|
|
148
|
+
progress: {
|
|
149
|
+
total,
|
|
150
|
+
current,
|
|
151
|
+
percent: (0, math_1.progressPercent)(total, current),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
155
157
|
});
|
|
156
158
|
}
|
|
157
159
|
exports.createTar = createTar;
|
|
@@ -164,10 +166,11 @@ function toLocalPath(path) {
|
|
|
164
166
|
async function extractTar(options) {
|
|
165
167
|
let total = options.total ??
|
|
166
168
|
(await listTar({ input: options.input, verbose: options.verbose }));
|
|
167
|
-
if (options.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
if (!(await (0, fs_1.existsDir)(options.output))) {
|
|
170
|
+
if (options.verbose)
|
|
171
|
+
(0, cli_1.logExec)("mkdir", ["-p", options.output]);
|
|
172
|
+
await (0, promises_1.mkdir)(options.output, { recursive: true });
|
|
173
|
+
}
|
|
171
174
|
const decompress = await ifX(options.decompress, async (decompress) => ({
|
|
172
175
|
...decompress,
|
|
173
176
|
cores: await resolveCores(decompress.cores),
|
|
@@ -192,20 +195,22 @@ async function extractTar(options) {
|
|
|
192
195
|
stderr: {
|
|
193
196
|
toExitCode: true,
|
|
194
197
|
},
|
|
195
|
-
stdout:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
198
|
+
stdout: options.onEntry
|
|
199
|
+
? {
|
|
200
|
+
parseLines: "skip-empty",
|
|
201
|
+
onData: (path) => {
|
|
202
|
+
current++;
|
|
203
|
+
options.onEntry?.({
|
|
204
|
+
path,
|
|
205
|
+
progress: {
|
|
206
|
+
total,
|
|
207
|
+
current,
|
|
208
|
+
percent: (0, math_1.progressPercent)(total, current),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
: undefined,
|
|
209
214
|
});
|
|
210
215
|
}
|
|
211
216
|
exports.extractTar = extractTar;
|