@datatruck/cli 0.21.1 → 0.22.1
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.js +1 -1
- package/Task/MysqlDumpTask.d.ts +13 -15
- package/Task/MysqlDumpTask.js +210 -149
- 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 +1 -0
- package/utils/fs.js +12 -4
- package/utils/mysql.d.ts +39 -0
- package/utils/mysql.js +224 -0
- package/utils/string.d.ts +5 -1
- package/utils/string.js +28 -6
package/Action/BackupAction.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ConfigType } from "../Config/Config";
|
|
2
2
|
import { PackageConfigType } from "../Config/PackageConfig";
|
|
3
3
|
import { RepositoryConfigType } from "../Config/RepositoryConfig";
|
|
4
|
-
import { AppError } from "../Error/AppError";
|
|
5
4
|
import { SnapshotType } from "../Repository/RepositoryAbstract";
|
|
6
5
|
import { BackupSessionManager } from "../SessionManager/BackupSessionManager";
|
|
7
6
|
import { TaskAbstract } from "../Task/TaskAbstract";
|
|
@@ -35,7 +34,7 @@ export declare class BackupAction<TRequired extends boolean = true> {
|
|
|
35
34
|
error: boolean;
|
|
36
35
|
tmpDirs: string[];
|
|
37
36
|
}>;
|
|
38
|
-
protected getError(pkg: PackageConfigType):
|
|
37
|
+
protected getError(pkg: PackageConfigType): Error | undefined;
|
|
39
38
|
protected splitRepositories(repositoryNames: string[]): {
|
|
40
39
|
repoNames: string[];
|
|
41
40
|
mirrors: {
|
|
@@ -45,6 +44,6 @@ export declare class BackupAction<TRequired extends boolean = true> {
|
|
|
45
44
|
};
|
|
46
45
|
exec(session: BackupSessionManager): Promise<{
|
|
47
46
|
total: number;
|
|
48
|
-
errors:
|
|
47
|
+
errors: Error[];
|
|
49
48
|
}>;
|
|
50
49
|
}
|
package/Action/BackupAction.js
CHANGED
|
@@ -67,7 +67,7 @@ class BackupAction {
|
|
|
67
67
|
const key = `${pkg.name}`;
|
|
68
68
|
let error;
|
|
69
69
|
if (this.taskErrors[key]?.length) {
|
|
70
|
-
error =
|
|
70
|
+
error = AppError_1.AppError.create("Previous task failed", this.taskErrors[key]);
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
73
|
try {
|
|
@@ -110,7 +110,7 @@ class BackupAction {
|
|
|
110
110
|
let error;
|
|
111
111
|
let repoInstance;
|
|
112
112
|
if (this.taskErrors[pkg.name]?.length) {
|
|
113
|
-
error =
|
|
113
|
+
error = AppError_1.AppError.create("Task failed", this.taskErrors[pkg.name]);
|
|
114
114
|
}
|
|
115
115
|
else {
|
|
116
116
|
try {
|
|
@@ -156,7 +156,7 @@ class BackupAction {
|
|
|
156
156
|
let error;
|
|
157
157
|
let repoInstance;
|
|
158
158
|
if (this.taskErrors[pkg.name]?.length) {
|
|
159
|
-
error =
|
|
159
|
+
error = AppError_1.AppError.create("Task failed", this.taskErrors[pkg.name]);
|
|
160
160
|
}
|
|
161
161
|
else {
|
|
162
162
|
try {
|
|
@@ -190,20 +190,16 @@ class BackupAction {
|
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
192
|
getError(pkg) {
|
|
193
|
-
const taskErrors = this.taskErrors[pkg.name]
|
|
194
|
-
const repoErrors = this.repoErrors[pkg.name]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
193
|
+
const taskErrors = this.taskErrors[pkg.name] || [];
|
|
194
|
+
const repoErrors = this.repoErrors[pkg.name] || [];
|
|
195
|
+
const errors = [...taskErrors, ...repoErrors];
|
|
196
|
+
if (!errors.length)
|
|
197
|
+
return;
|
|
198
|
+
return AppError_1.AppError.create(taskErrors.length && repoErrors.length
|
|
199
|
+
? "Task and repository failed"
|
|
200
|
+
: taskErrors.length && !repoErrors.length
|
|
201
|
+
? "Task failed"
|
|
202
|
+
: "Repository failed", errors);
|
|
207
203
|
}
|
|
208
204
|
splitRepositories(repositoryNames) {
|
|
209
205
|
const mirrorRepoMap = {};
|
|
@@ -230,7 +226,7 @@ class BackupAction {
|
|
|
230
226
|
}
|
|
231
227
|
async exec(session) {
|
|
232
228
|
const [snapshot, packages] = await this.init(session);
|
|
233
|
-
|
|
229
|
+
const errors = [];
|
|
234
230
|
for (const pkg of packages) {
|
|
235
231
|
const id = session.findId({
|
|
236
232
|
packageName: pkg.name,
|
|
@@ -272,7 +268,7 @@ class BackupAction {
|
|
|
272
268
|
}
|
|
273
269
|
const error = this.getError(pkg);
|
|
274
270
|
if (error)
|
|
275
|
-
errors
|
|
271
|
+
errors.push(error);
|
|
276
272
|
await session.end({
|
|
277
273
|
id: id,
|
|
278
274
|
error: error?.message,
|
|
@@ -283,7 +279,7 @@ class BackupAction {
|
|
|
283
279
|
});
|
|
284
280
|
return {
|
|
285
281
|
total: packages.length,
|
|
286
|
-
errors
|
|
282
|
+
errors,
|
|
287
283
|
};
|
|
288
284
|
}
|
|
289
285
|
}
|
|
@@ -39,6 +39,8 @@ export declare class RestoreAction<TRequired extends boolean = true> {
|
|
|
39
39
|
tmpDirs: string[];
|
|
40
40
|
}>;
|
|
41
41
|
protected getError(pkg: PackageConfigType): AppError | null;
|
|
42
|
-
exec(session: RestoreSessionManager): Promise<
|
|
42
|
+
exec(session: RestoreSessionManager): Promise<{
|
|
43
|
+
errors: Error[];
|
|
44
|
+
}>;
|
|
43
45
|
}
|
|
44
46
|
export {};
|
package/Action/RestoreAction.js
CHANGED
|
@@ -100,10 +100,10 @@ class RestoreAction {
|
|
|
100
100
|
});
|
|
101
101
|
let error;
|
|
102
102
|
if (this.repoErrors[pkg.name]?.length) {
|
|
103
|
-
error =
|
|
103
|
+
error = AppError_1.AppError.create("Repository failed", this.repoErrors[pkg.name]);
|
|
104
104
|
}
|
|
105
105
|
else if (this.taskErrors[pkg.name]?.length) {
|
|
106
|
-
error =
|
|
106
|
+
error = AppError_1.AppError.create("Previous task failed", this.taskErrors[pkg.name]);
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
109
|
try {
|
|
@@ -222,7 +222,7 @@ class RestoreAction {
|
|
|
222
222
|
});
|
|
223
223
|
const snapshotAndConfigs = this.assocConfigs(packages, snapshots);
|
|
224
224
|
await this.init(session, this.options.snapshotId, snapshotAndConfigs);
|
|
225
|
-
|
|
225
|
+
const errors = [];
|
|
226
226
|
for (const [snapshot, pkg] of snapshotAndConfigs) {
|
|
227
227
|
(0, assert_1.ok)(pkg);
|
|
228
228
|
const repo = (0, config_1.findRepositoryOrFail)(this.config, snapshot.repositoryName);
|
|
@@ -257,10 +257,10 @@ class RestoreAction {
|
|
|
257
257
|
error: error?.message,
|
|
258
258
|
});
|
|
259
259
|
if (error)
|
|
260
|
-
|
|
260
|
+
errors.push(error);
|
|
261
261
|
}
|
|
262
262
|
await session.endDrivers();
|
|
263
|
-
return
|
|
263
|
+
return { errors };
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
exports.RestoreAction = RestoreAction;
|
|
@@ -12,5 +12,5 @@ export type BackupCommandOptionsType<TResolved = false> = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare class BackupCommand extends CommandAbstract<BackupCommandOptionsType<false>, BackupCommandOptionsType<true>> {
|
|
14
14
|
onOptions(): import("../utils/cli").OptionsType<BackupCommandOptionsType<false>, BackupCommandOptionsType<true>>;
|
|
15
|
-
onExec(): Promise<
|
|
15
|
+
onExec(): Promise<1 | 0>;
|
|
16
16
|
}
|
package/Command/BackupCommand.js
CHANGED
|
@@ -74,7 +74,7 @@ class BackupCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
74
74
|
progressInterval: this.globalOptions.progressInterval,
|
|
75
75
|
});
|
|
76
76
|
const result = await backup.exec(sessionManager);
|
|
77
|
-
if (result.errors) {
|
|
77
|
+
if (result.errors.length) {
|
|
78
78
|
return 1;
|
|
79
79
|
}
|
|
80
80
|
else if (!result.total) {
|
|
@@ -12,5 +12,5 @@ export type RestoreCommandOptionsType<TResolved = false> = {
|
|
|
12
12
|
};
|
|
13
13
|
export declare class RestoreCommand extends CommandAbstract<RestoreCommandOptionsType<false>, RestoreCommandOptionsType<true>> {
|
|
14
14
|
onOptions(): import("../utils/cli").OptionsType<RestoreCommandOptionsType<false>, RestoreCommandOptionsType<true>>;
|
|
15
|
-
onExec(): Promise<
|
|
15
|
+
onExec(): Promise<1 | 0>;
|
|
16
16
|
}
|
|
@@ -74,7 +74,7 @@ class RestoreCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
74
74
|
progressInterval: this.globalOptions.progressInterval,
|
|
75
75
|
});
|
|
76
76
|
const result = await restore.exec(sessionManager);
|
|
77
|
-
return result ?
|
|
77
|
+
return result.errors.length ? 1 : 0;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
exports.RestoreCommand = RestoreCommand;
|
package/Error/AppError.d.ts
CHANGED
package/Error/AppError.js
CHANGED
|
@@ -6,5 +6,13 @@ class AppError extends Error {
|
|
|
6
6
|
super(message);
|
|
7
7
|
this.name = AppError.name;
|
|
8
8
|
}
|
|
9
|
+
static create(message, errors) {
|
|
10
|
+
if (errors.length === 1) {
|
|
11
|
+
return errors[0];
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return new AggregateError(errors, message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
exports.AppError = AppError;
|
package/JsonSchema/JsonSchema.js
CHANGED
|
@@ -38,7 +38,7 @@ exports.definitions = {
|
|
|
38
38
|
[DefinitionEnum_1.DefinitionEnum.resticPackageRepository]: ResticRepository_1.resticPackageRepositoryDefinition,
|
|
39
39
|
[DefinitionEnum_1.DefinitionEnum.gitTask]: GitTask_1.gitTaskDefinition,
|
|
40
40
|
[DefinitionEnum_1.DefinitionEnum.scriptTask]: ScriptTask_1.scriptTaskDefinition,
|
|
41
|
-
[DefinitionEnum_1.DefinitionEnum.sqlDumpTask]: SqlDumpTaskAbstract_1.sqlDumpTaskDefinition,
|
|
41
|
+
[DefinitionEnum_1.DefinitionEnum.sqlDumpTask]: (0, SqlDumpTaskAbstract_1.sqlDumpTaskDefinition)(),
|
|
42
42
|
[DefinitionEnum_1.DefinitionEnum.mariadbTask]: MariadbTask_1.mariadbTaskDefinition,
|
|
43
43
|
[DefinitionEnum_1.DefinitionEnum.mssqlTask]: MssqlTask_1.mssqlTaskDefinition,
|
|
44
44
|
[DefinitionEnum_1.DefinitionEnum.mysqlDumpTask]: MysqlDumpTask_1.mysqlDumpTaskDefinition,
|
|
@@ -196,7 +196,7 @@ class DatatruckRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
196
196
|
if (entry.dirent.isDirectory() &&
|
|
197
197
|
!(await (0, fs_1.isEmptyDir)((0, path_1.join)(sourcePath, entry.path))))
|
|
198
198
|
return false;
|
|
199
|
-
let packIndex = configPacks.findIndex((pack) => (0, string_1.
|
|
199
|
+
let packIndex = configPacks.findIndex((pack) => (0, string_1.match)(entry.path, pack.include, pack.exclude));
|
|
200
200
|
if (packIndex === -1)
|
|
201
201
|
packIndex = defaultsPackIndex;
|
|
202
202
|
const pack = packs[packIndex];
|
package/Task/MysqlDumpTask.d.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { SqlDumpTaskConfigType } from "./SqlDumpTaskAbstract";
|
|
2
|
+
import { BackupDataType, RestoreDataType, TaskAbstract } from "./TaskAbstract";
|
|
3
3
|
export declare const mysqlDumpTaskName = "mysql-dump";
|
|
4
|
-
export type MysqlDumpTaskConfigType = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
onExportStoredPrograms(output: string): Promise<void>;
|
|
16
|
-
onImport(path: string, database: string): Promise<void>;
|
|
4
|
+
export type MysqlDumpTaskConfigType = {
|
|
5
|
+
/**
|
|
6
|
+
* @default "sql"
|
|
7
|
+
*/
|
|
8
|
+
dataFormat?: "csv" | "sql";
|
|
9
|
+
csvSharedPath?: string;
|
|
10
|
+
} & SqlDumpTaskConfigType;
|
|
11
|
+
export declare const mysqlDumpTaskDefinition: import("json-schema").JSONSchema7;
|
|
12
|
+
export declare class MysqlDumpTask extends TaskAbstract<MysqlDumpTaskConfigType> {
|
|
13
|
+
onBackup(data: BackupDataType): Promise<void>;
|
|
14
|
+
onRestore(data: RestoreDataType): Promise<void>;
|
|
17
15
|
}
|
package/Task/MysqlDumpTask.js
CHANGED
|
@@ -2,165 +2,226 @@
|
|
|
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
|
|
5
|
+
const cli_1 = require("../utils/cli");
|
|
6
|
+
const config_1 = require("../utils/datatruck/config");
|
|
6
7
|
const fs_1 = require("../utils/fs");
|
|
7
|
-
const
|
|
8
|
+
const math_1 = require("../utils/math");
|
|
9
|
+
const mysql_1 = require("../utils/mysql");
|
|
10
|
+
const string_1 = require("../utils/string");
|
|
8
11
|
const SqlDumpTaskAbstract_1 = require("./SqlDumpTaskAbstract");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
12
|
+
const TaskAbstract_1 = require("./TaskAbstract");
|
|
13
|
+
const assert_1 = require("assert");
|
|
14
|
+
const promises_1 = require("fs/promises");
|
|
15
|
+
const path_1 = require("path");
|
|
11
16
|
exports.mysqlDumpTaskName = "mysql-dump";
|
|
12
|
-
exports.mysqlDumpTaskDefinition = {
|
|
13
|
-
|
|
17
|
+
exports.mysqlDumpTaskDefinition = (0, SqlDumpTaskAbstract_1.sqlDumpTaskDefinition)({
|
|
18
|
+
dataFormat: { enum: ["csv", "sql"] },
|
|
19
|
+
csvSharedPath: { type: "string" },
|
|
20
|
+
});
|
|
21
|
+
const suffix = {
|
|
22
|
+
database: ".database.sql",
|
|
23
|
+
stored: ".stored-programs.sql",
|
|
24
|
+
table: ".table.sql",
|
|
25
|
+
tableData: ".table-data.csv",
|
|
26
|
+
tableSchema: ".table-schema.sql",
|
|
14
27
|
};
|
|
15
|
-
class MysqlDumpTask extends
|
|
16
|
-
async
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
...(this.config.port ? [`--port=${this.config.port}`] : []),
|
|
21
|
-
`--user=${this.config.username}`,
|
|
22
|
-
`--password=${password ?? ""}`,
|
|
23
|
-
...(database ? [database] : []),
|
|
24
|
-
];
|
|
25
|
-
}
|
|
26
|
-
async onDatabaseIsEmpty(name) {
|
|
27
|
-
const [total] = await this.fetchValues(`
|
|
28
|
-
SELECT
|
|
29
|
-
COUNT(*) AS total
|
|
30
|
-
FROM
|
|
31
|
-
information_schema.tables
|
|
32
|
-
WHERE
|
|
33
|
-
table_schema = '${name}'
|
|
34
|
-
`);
|
|
35
|
-
return Number(total) ? false : true;
|
|
36
|
-
}
|
|
37
|
-
async onCreateDatabase(database) {
|
|
38
|
-
const query = `
|
|
39
|
-
CREATE DATABASE IF NOT EXISTS \`${database.name}\`
|
|
40
|
-
CHARACTER SET ${database.charset ?? "utf8"}
|
|
41
|
-
COLLATE ${database.charset ?? "utf8_general_ci"}
|
|
42
|
-
`;
|
|
43
|
-
await this.onExecQuery(query);
|
|
44
|
-
}
|
|
45
|
-
async onExecQuery(query) {
|
|
46
|
-
return await (0, process_1.exec)("mysql", [
|
|
47
|
-
...(await this.buildConnectionArgs()),
|
|
48
|
-
"-e",
|
|
49
|
-
query.replace(/\s{1,}/g, " "),
|
|
50
|
-
"-N",
|
|
51
|
-
], undefined, {
|
|
52
|
-
log: this.verbose,
|
|
53
|
-
stderr: {
|
|
54
|
-
toExitCode: true,
|
|
55
|
-
},
|
|
56
|
-
stdout: {
|
|
57
|
-
save: true,
|
|
58
|
-
},
|
|
28
|
+
class MysqlDumpTask extends TaskAbstract_1.TaskAbstract {
|
|
29
|
+
async onBackup(data) {
|
|
30
|
+
const sql = (0, mysql_1.createMysqlCli)({
|
|
31
|
+
...this.config,
|
|
32
|
+
verbose: data.options.verbose,
|
|
59
33
|
});
|
|
34
|
+
const tableNames = await sql.fetchTableNames(this.config.database, this.config.includeTables, this.config.excludeTables);
|
|
35
|
+
const outputPath = data.package.path;
|
|
36
|
+
(0, assert_1.ok)(typeof outputPath === "string");
|
|
37
|
+
const dataFormat = this.config.dataFormat ?? "sql";
|
|
38
|
+
await (0, promises_1.mkdir)(outputPath, { recursive: true });
|
|
39
|
+
const sharedDir = dataFormat === "csv"
|
|
40
|
+
? await sql.initSharedDir(this.config.csvSharedPath)
|
|
41
|
+
: undefined;
|
|
42
|
+
if (this.config.oneFileByTable || sharedDir) {
|
|
43
|
+
let current = 0;
|
|
44
|
+
for (const tableName of tableNames) {
|
|
45
|
+
await data.onProgress({
|
|
46
|
+
relative: {
|
|
47
|
+
description: "Exporting",
|
|
48
|
+
payload: tableName,
|
|
49
|
+
},
|
|
50
|
+
absolute: {
|
|
51
|
+
total: tableNames.length,
|
|
52
|
+
current,
|
|
53
|
+
percent: (0, math_1.progressPercent)(tableNames.length, current),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (sharedDir) {
|
|
57
|
+
const tableSharedPath = (0, path_1.join)(sharedDir, `tmp-dtt-backup-${data.snapshot.id.slice(0, 8)}-${tableName}`);
|
|
58
|
+
if (data.options.verbose) {
|
|
59
|
+
(0, cli_1.logExec)("mkdir", ["-p", tableSharedPath]);
|
|
60
|
+
(0, cli_1.logExec)("chmod", ["777", tableSharedPath]);
|
|
61
|
+
}
|
|
62
|
+
await (0, promises_1.mkdir)(tableSharedPath, { recursive: true });
|
|
63
|
+
await (0, promises_1.chmod)(tableSharedPath, 0o777);
|
|
64
|
+
try {
|
|
65
|
+
await sql.csvDump({
|
|
66
|
+
sharedPath: tableSharedPath,
|
|
67
|
+
items: [tableName],
|
|
68
|
+
database: this.config.database,
|
|
69
|
+
});
|
|
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
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
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
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await data.onProgress({
|
|
112
|
+
relative: { description: "Exporting" },
|
|
113
|
+
});
|
|
114
|
+
await sql.dump({
|
|
115
|
+
output: (0, path_1.join)(outputPath, `${this.config.database}${suffix.database}`),
|
|
116
|
+
items: tableNames,
|
|
117
|
+
database: this.config.database,
|
|
118
|
+
onProgress: (progress) => data.onProgress({
|
|
119
|
+
absolute: {
|
|
120
|
+
description: "Exporting in single file",
|
|
121
|
+
current: progress.totalBytes,
|
|
122
|
+
format: "size",
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (this.config.storedPrograms ?? true) {
|
|
128
|
+
await data.onProgress({
|
|
129
|
+
relative: { description: "Exporting stored programs" },
|
|
130
|
+
});
|
|
131
|
+
await sql.dump({
|
|
132
|
+
database: this.config.database,
|
|
133
|
+
output: (0, path_1.join)(outputPath, `${this.config.database}${suffix.stored}`),
|
|
134
|
+
onlyStoredPrograms: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
60
137
|
}
|
|
61
|
-
async
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
FROM
|
|
66
|
-
information_schema.tables
|
|
67
|
-
WHERE
|
|
68
|
-
table_schema = '${database}'
|
|
69
|
-
ORDER BY
|
|
70
|
-
table_name
|
|
71
|
-
`);
|
|
72
|
-
}
|
|
73
|
-
async onExportTables(tableNames, output, onProgress) {
|
|
74
|
-
const stream = (0, fs_2.createWriteStream)(output);
|
|
75
|
-
await Promise.all([
|
|
76
|
-
new Promise((resolve, reject) => {
|
|
77
|
-
stream.on("close", resolve);
|
|
78
|
-
stream.on("error", reject);
|
|
79
|
-
}),
|
|
80
|
-
await (0, process_1.exec)("mysqldump", [
|
|
81
|
-
...(await this.buildConnectionArgs(this.config.database)),
|
|
82
|
-
"--lock-tables=false",
|
|
83
|
-
"--skip-add-drop-table=false",
|
|
84
|
-
...tableNames,
|
|
85
|
-
], null, {
|
|
86
|
-
pipe: {
|
|
87
|
-
stream,
|
|
88
|
-
onWriteProgress: onProgress,
|
|
89
|
-
},
|
|
90
|
-
log: {
|
|
91
|
-
exec: this.verbose,
|
|
92
|
-
stderr: this.verbose,
|
|
93
|
-
allToStderr: true,
|
|
94
|
-
},
|
|
95
|
-
stderr: {
|
|
96
|
-
toExitCode: true,
|
|
97
|
-
},
|
|
98
|
-
}),
|
|
99
|
-
]);
|
|
100
|
-
const headerContents = await (0, fs_1.readPartialFile)(output, [0, 100]);
|
|
101
|
-
const footerContents = await (0, fs_1.readPartialFile)(output, [-100]);
|
|
102
|
-
const successHeader = headerContents.split(/\r?\n/).some((line) => {
|
|
103
|
-
const firstLine = line.trim().toLowerCase();
|
|
104
|
-
return (firstLine.startsWith("-- mysql dump") ||
|
|
105
|
-
firstLine.startsWith("-- mariadb dump"));
|
|
138
|
+
async onRestore(data) {
|
|
139
|
+
const sql = (0, mysql_1.createMysqlCli)({
|
|
140
|
+
...this.config,
|
|
141
|
+
verbose: data.options.verbose,
|
|
106
142
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
const restorePath = data.package.restorePath;
|
|
144
|
+
(0, assert_1.ok)(typeof restorePath === "string");
|
|
145
|
+
const params = {
|
|
146
|
+
packageName: data.package.name,
|
|
147
|
+
snapshotId: data.options.snapshotId,
|
|
148
|
+
snapshotDate: data.snapshot.date,
|
|
149
|
+
action: "restore",
|
|
150
|
+
database: undefined,
|
|
151
|
+
};
|
|
152
|
+
const database = {
|
|
153
|
+
name: (0, config_1.resolveDatabaseName)(this.config.database, params),
|
|
154
|
+
};
|
|
155
|
+
if (this.config.targetDatabase)
|
|
156
|
+
database.name = (0, config_1.resolveDatabaseName)(this.config.targetDatabase.name, {
|
|
157
|
+
...params,
|
|
158
|
+
database: database.name,
|
|
159
|
+
});
|
|
160
|
+
const suffixes = Object.values(suffix);
|
|
161
|
+
const files = (await (0, fs_1.readDir)(restorePath)).filter((f) => (0, string_1.endsWith)(f, suffixes));
|
|
162
|
+
// Database check
|
|
163
|
+
if (files.some((f) => f.endsWith(suffix.database)) &&
|
|
164
|
+
!(await sql.isDatabaseEmpty(database.name)))
|
|
165
|
+
throw new AppError_1.AppError(`Target database is not empty: ${database.name}`);
|
|
166
|
+
// Table check
|
|
167
|
+
const restoreTables = [
|
|
168
|
+
...new Set(...files
|
|
169
|
+
.filter((f) => (0, string_1.endsWith)(f, [suffix.table, suffix.tableSchema, suffix.tableData]))
|
|
170
|
+
.map((f) => f.split(".")[0])),
|
|
171
|
+
];
|
|
172
|
+
const serverTables = await sql.fetchTableNames(database.name);
|
|
173
|
+
const errorTables = restoreTables.filter((v) => serverTables.includes(v));
|
|
174
|
+
if (errorTables.length)
|
|
175
|
+
throw new AppError_1.AppError(`Target table already exists: ${errorTables.join(", ")}`);
|
|
176
|
+
// Data check
|
|
177
|
+
const dataFiles = files.filter((f) => f.endsWith(suffix.tableData));
|
|
178
|
+
const sharedDir = dataFiles.length
|
|
179
|
+
? await sql.initSharedDir(this.config.csvSharedPath)
|
|
180
|
+
: undefined;
|
|
181
|
+
await sql.createDatabase(database);
|
|
182
|
+
if (data.options.verbose)
|
|
183
|
+
(0, cli_1.logExec)("readdir", [restorePath]);
|
|
184
|
+
let current = 0;
|
|
185
|
+
for (const file of files.filter((f) => !f.endsWith(suffix.tableData))) {
|
|
186
|
+
const path = (0, path_1.join)(restorePath, file);
|
|
187
|
+
data.onProgress({
|
|
188
|
+
relative: {
|
|
189
|
+
description: "Importing",
|
|
190
|
+
payload: file,
|
|
138
191
|
},
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
absolute: {
|
|
193
|
+
total: files.length,
|
|
194
|
+
current: current,
|
|
195
|
+
percent: (0, math_1.progressPercent)(files.length, current),
|
|
141
196
|
},
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
colorize: true,
|
|
154
|
-
stderr: true,
|
|
155
|
-
lineSalt: true,
|
|
156
|
-
});
|
|
197
|
+
});
|
|
198
|
+
await sql.importFile(path, database.name);
|
|
199
|
+
current++;
|
|
200
|
+
}
|
|
201
|
+
for (const file of dataFiles) {
|
|
202
|
+
const filePath = (0, path_1.join)(restorePath, file);
|
|
203
|
+
const tableName = file.slice(0, suffix.tableData.length * -1);
|
|
204
|
+
data.onProgress({
|
|
205
|
+
relative: {
|
|
206
|
+
description: "Importing",
|
|
207
|
+
payload: file,
|
|
157
208
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
209
|
+
absolute: {
|
|
210
|
+
total: files.length,
|
|
211
|
+
current: current,
|
|
212
|
+
percent: (0, math_1.progressPercent)(files.length, current),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
const sharedFilePath = (0, path_1.join)(sharedDir, `tmp-dtt-restore-${data.snapshot.id.slice(0, 8)}-${tableName}.data.csv`);
|
|
216
|
+
try {
|
|
217
|
+
await (0, promises_1.rename)(filePath, sharedFilePath);
|
|
218
|
+
await sql.importCsvFile(sharedFilePath, database.name, tableName);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
await (0, promises_1.rm)(sharedFilePath);
|
|
222
|
+
}
|
|
223
|
+
current++;
|
|
224
|
+
}
|
|
164
225
|
}
|
|
165
226
|
}
|
|
166
227
|
exports.MysqlDumpTask = MysqlDumpTask;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { exec } from "../utils/process";
|
|
2
2
|
import { BackupDataType, RestoreDataType, TaskAbstract } from "./TaskAbstract";
|
|
3
|
-
import { JSONSchema7 } from "json-schema";
|
|
3
|
+
import { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
4
4
|
export type TargetDatabaseType = {
|
|
5
5
|
name: string;
|
|
6
6
|
charset?: string;
|
|
@@ -14,13 +14,16 @@ export type SqlDumpTaskConfigType = {
|
|
|
14
14
|
port?: number;
|
|
15
15
|
database: string;
|
|
16
16
|
username: string;
|
|
17
|
+
/**
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
17
20
|
storedPrograms?: boolean;
|
|
18
21
|
targetDatabase?: TargetDatabaseType;
|
|
19
22
|
includeTables?: string[];
|
|
20
23
|
excludeTables?: string[];
|
|
21
24
|
oneFileByTable?: boolean;
|
|
22
25
|
};
|
|
23
|
-
export declare const sqlDumpTaskDefinition: JSONSchema7;
|
|
26
|
+
export declare const sqlDumpTaskDefinition: (props?: Record<string, JSONSchema7Definition>) => JSONSchema7;
|
|
24
27
|
export declare abstract class SqlDumpTaskAbstract<TConfig extends SqlDumpTaskConfigType> extends TaskAbstract<TConfig> {
|
|
25
28
|
protected verbose?: boolean;
|
|
26
29
|
fetchPassword(): Promise<string | null>;
|
|
@@ -12,11 +12,12 @@ const assert_1 = require("assert");
|
|
|
12
12
|
const promises_1 = require("fs/promises");
|
|
13
13
|
const micromatch_1 = require("micromatch");
|
|
14
14
|
const path_1 = require("path");
|
|
15
|
-
|
|
15
|
+
const sqlDumpTaskDefinition = (props = {}) => ({
|
|
16
16
|
type: "object",
|
|
17
17
|
required: ["password", "hostname", "username", "database"],
|
|
18
18
|
additionalProperties: false,
|
|
19
19
|
properties: {
|
|
20
|
+
...props,
|
|
20
21
|
password: {
|
|
21
22
|
anyOf: [
|
|
22
23
|
{
|
|
@@ -50,7 +51,8 @@ exports.sqlDumpTaskDefinition = {
|
|
|
50
51
|
excludeTables: (0, DefinitionEnum_1.makeRef)(DefinitionEnum_1.DefinitionEnum.stringListUtil),
|
|
51
52
|
oneFileByTable: { type: "boolean" },
|
|
52
53
|
},
|
|
53
|
-
};
|
|
54
|
+
});
|
|
55
|
+
exports.sqlDumpTaskDefinition = sqlDumpTaskDefinition;
|
|
54
56
|
function serializeSqlFile(input) {
|
|
55
57
|
if (input.database && input.table) {
|
|
56
58
|
return `${input.database}.${input.table}.table.sql`;
|
package/config.schema.json
CHANGED
|
@@ -880,11 +880,85 @@
|
|
|
880
880
|
}
|
|
881
881
|
},
|
|
882
882
|
"mysql-dump-task": {
|
|
883
|
-
"
|
|
884
|
-
|
|
885
|
-
|
|
883
|
+
"type": "object",
|
|
884
|
+
"required": [
|
|
885
|
+
"password",
|
|
886
|
+
"hostname",
|
|
887
|
+
"username",
|
|
888
|
+
"database"
|
|
889
|
+
],
|
|
890
|
+
"additionalProperties": false,
|
|
891
|
+
"properties": {
|
|
892
|
+
"dataFormat": {
|
|
893
|
+
"enum": [
|
|
894
|
+
"csv",
|
|
895
|
+
"sql"
|
|
896
|
+
]
|
|
897
|
+
},
|
|
898
|
+
"csvSharedPath": {
|
|
899
|
+
"type": "string"
|
|
900
|
+
},
|
|
901
|
+
"password": {
|
|
902
|
+
"anyOf": [
|
|
903
|
+
{
|
|
904
|
+
"type": "string"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
"type": "object",
|
|
908
|
+
"additionalProperties": false,
|
|
909
|
+
"required": [
|
|
910
|
+
"path"
|
|
911
|
+
],
|
|
912
|
+
"properties": {
|
|
913
|
+
"path": {
|
|
914
|
+
"type": "string"
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
"hostname": {
|
|
921
|
+
"type": "string"
|
|
922
|
+
},
|
|
923
|
+
"port": {
|
|
924
|
+
"type": "integer"
|
|
925
|
+
},
|
|
926
|
+
"username": {
|
|
927
|
+
"type": "string"
|
|
928
|
+
},
|
|
929
|
+
"database": {
|
|
930
|
+
"type": "string"
|
|
931
|
+
},
|
|
932
|
+
"targetDatabase": {
|
|
933
|
+
"type": "object",
|
|
934
|
+
"required": [
|
|
935
|
+
"name"
|
|
936
|
+
],
|
|
937
|
+
"properties": {
|
|
938
|
+
"name": {
|
|
939
|
+
"type": "string"
|
|
940
|
+
},
|
|
941
|
+
"charset": {
|
|
942
|
+
"type": "string"
|
|
943
|
+
},
|
|
944
|
+
"collate": {
|
|
945
|
+
"type": "string"
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
"storedPrograms": {
|
|
950
|
+
"type": "boolean"
|
|
951
|
+
},
|
|
952
|
+
"includeTables": {
|
|
953
|
+
"$ref": "#/definitions/stringlist-util"
|
|
954
|
+
},
|
|
955
|
+
"excludeTables": {
|
|
956
|
+
"$ref": "#/definitions/stringlist-util"
|
|
957
|
+
},
|
|
958
|
+
"oneFileByTable": {
|
|
959
|
+
"type": "boolean"
|
|
886
960
|
}
|
|
887
|
-
|
|
961
|
+
}
|
|
888
962
|
},
|
|
889
963
|
"postgresql-dump-task": {
|
|
890
964
|
"allOf": [
|
package/package.json
CHANGED
package/utils/cli.js
CHANGED
|
@@ -65,7 +65,9 @@ function logExec(command, argv = [], env, logToStderr) {
|
|
|
65
65
|
.join(" ")
|
|
66
66
|
: "";
|
|
67
67
|
const text = `+ ${envText ? envText + " " : ""}${chalk_1.default.yellow(`${command} ${argv.join(" ")}`)}`;
|
|
68
|
-
logToStderr
|
|
68
|
+
logToStderr /* && process.env.VITEST !== "true"*/
|
|
69
|
+
? process.stderr.write(`${text}\n`)
|
|
70
|
+
: console.info(text);
|
|
69
71
|
}
|
|
70
72
|
exports.logExec = logExec;
|
|
71
73
|
function resultColumn(error, state) {
|
|
@@ -15,7 +15,7 @@ type ResolvePackagePathParamsType = ResolvePackageParamsType & {
|
|
|
15
15
|
path: string | undefined;
|
|
16
16
|
};
|
|
17
17
|
export declare function resolvePackagePath(value: string, params: ResolvePackagePathParamsType): string;
|
|
18
|
-
type ResolveDatabaseNameParamsType = ResolvePackageParamsType & {
|
|
18
|
+
export type ResolveDatabaseNameParamsType = ResolvePackageParamsType & {
|
|
19
19
|
packageName: string;
|
|
20
20
|
database: string | undefined;
|
|
21
21
|
};
|
package/utils/fs.d.ts
CHANGED
|
@@ -119,4 +119,5 @@ export declare function createWriteStreamPool(options: {
|
|
|
119
119
|
end(): Promise<void>;
|
|
120
120
|
};
|
|
121
121
|
export declare function countFileLines(path: string): Promise<number>;
|
|
122
|
+
export declare function fetchData<T>(input: T, onPath?: (input: Exclude<T, string>) => string | undefined): Promise<string | null>;
|
|
122
123
|
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.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.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");
|
|
@@ -43,9 +43,8 @@ function isLocalDir(path) {
|
|
|
43
43
|
}
|
|
44
44
|
exports.isLocalDir = isLocalDir;
|
|
45
45
|
async function mkdirIfNotExists(path) {
|
|
46
|
-
|
|
47
|
-
recursive: true
|
|
48
|
-
});
|
|
46
|
+
if (!(await existsDir(path)))
|
|
47
|
+
await (0, promises_1.mkdir)(path, { recursive: true });
|
|
49
48
|
return path;
|
|
50
49
|
}
|
|
51
50
|
exports.mkdirIfNotExists = mkdirIfNotExists;
|
|
@@ -536,3 +535,12 @@ function countFileLines(path) {
|
|
|
536
535
|
});
|
|
537
536
|
}
|
|
538
537
|
exports.countFileLines = countFileLines;
|
|
538
|
+
async function fetchData(input, onPath) {
|
|
539
|
+
if (typeof input === "string")
|
|
540
|
+
return input;
|
|
541
|
+
const path = onPath?.(input);
|
|
542
|
+
if (typeof path === "string")
|
|
543
|
+
return (await (0, promises_1.readFile)(path)).toString();
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
exports.fetchData = fetchData;
|
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, extra?: 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,224 @@
|
|
|
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
|
+
let defaultsFilePath;
|
|
15
|
+
async function getDefaultsFilePath() {
|
|
16
|
+
if (defaultsFilePath)
|
|
17
|
+
return defaultsFilePath;
|
|
18
|
+
const dir = await (0, fs_1.mkTmpDir)("mysql-cli");
|
|
19
|
+
const password = await (0, fs_1.fetchData)(options.password, (p) => p.path);
|
|
20
|
+
const data = [
|
|
21
|
+
`[client]`,
|
|
22
|
+
`host = "${options.hostname}"`,
|
|
23
|
+
...(options.port ? [`port = "${options.port}"`] : []),
|
|
24
|
+
`user = "${options.username}"`,
|
|
25
|
+
`password = "${password}"`,
|
|
26
|
+
];
|
|
27
|
+
console.log(data.join("\n"));
|
|
28
|
+
await (0, promises_1.writeFile)((defaultsFilePath = (0, path_1.join)(dir, "mysql.conf")), data.join("\n"));
|
|
29
|
+
return defaultsFilePath;
|
|
30
|
+
}
|
|
31
|
+
async function args() {
|
|
32
|
+
return [`--defaults-file=${await getDefaultsFilePath()}`];
|
|
33
|
+
}
|
|
34
|
+
async function run(query, database, extra = []) {
|
|
35
|
+
return await (0, process_1.exec)("mysql", [
|
|
36
|
+
...(await args()),
|
|
37
|
+
...(database ? [database] : []),
|
|
38
|
+
...(extra || []),
|
|
39
|
+
"-e",
|
|
40
|
+
query.replace(/\s{1,}/g, " "),
|
|
41
|
+
"-N",
|
|
42
|
+
"--silent",
|
|
43
|
+
], undefined, {
|
|
44
|
+
log: options.verbose,
|
|
45
|
+
stderr: { toExitCode: true },
|
|
46
|
+
stdout: { save: true },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function fetchAll(query, database) {
|
|
50
|
+
return (0, string_1.splitLines)((await run(query, database)).stdout).map((line) => line.split("\t"));
|
|
51
|
+
}
|
|
52
|
+
async function fetchTableNames(database, include, exclude) {
|
|
53
|
+
return (await fetchAll(`
|
|
54
|
+
SELECT
|
|
55
|
+
table_name
|
|
56
|
+
FROM
|
|
57
|
+
information_schema.tables
|
|
58
|
+
WHERE
|
|
59
|
+
table_schema = '${database}'
|
|
60
|
+
ORDER BY
|
|
61
|
+
table_name
|
|
62
|
+
`))
|
|
63
|
+
.map((r) => r[0])
|
|
64
|
+
.filter((0, string_1.createMatchFilter)(include, exclude));
|
|
65
|
+
}
|
|
66
|
+
async function dump(input) {
|
|
67
|
+
const stream = (0, fs_2.createWriteStream)(input.output);
|
|
68
|
+
return await Promise.all([
|
|
69
|
+
new Promise((resolve, reject) => {
|
|
70
|
+
stream.on("close", resolve);
|
|
71
|
+
stream.on("error", reject);
|
|
72
|
+
}),
|
|
73
|
+
await (0, process_1.exec)("mysqldump", [
|
|
74
|
+
...(await args()),
|
|
75
|
+
input.database,
|
|
76
|
+
"--lock-tables=false",
|
|
77
|
+
"--skip-add-drop-table=false",
|
|
78
|
+
...(input.onlyStoredPrograms
|
|
79
|
+
? [
|
|
80
|
+
"--routines",
|
|
81
|
+
"--events",
|
|
82
|
+
"--skip-triggers",
|
|
83
|
+
"--no-create-info",
|
|
84
|
+
"--no-data",
|
|
85
|
+
"--no-create-db",
|
|
86
|
+
"--skip-opt",
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
89
|
+
...(input.items || []),
|
|
90
|
+
], null, {
|
|
91
|
+
stderr: { toExitCode: true },
|
|
92
|
+
pipe: {
|
|
93
|
+
stream,
|
|
94
|
+
onWriteProgress: input.onProgress,
|
|
95
|
+
},
|
|
96
|
+
log: {
|
|
97
|
+
exec: options.verbose,
|
|
98
|
+
stderr: options.verbose,
|
|
99
|
+
allToStderr: true,
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
async function csvDump(input) {
|
|
105
|
+
await (0, process_1.exec)("mysqldump", [
|
|
106
|
+
...(await args()),
|
|
107
|
+
input.database,
|
|
108
|
+
"--lock-tables=false",
|
|
109
|
+
"--skip-add-drop-table=false",
|
|
110
|
+
"-T",
|
|
111
|
+
input.sharedPath,
|
|
112
|
+
...(input.items || []),
|
|
113
|
+
], null, {
|
|
114
|
+
stderr: { toExitCode: true },
|
|
115
|
+
log: {
|
|
116
|
+
exec: options.verbose,
|
|
117
|
+
stderr: options.verbose,
|
|
118
|
+
allToStderr: true,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function importFile(path, database) {
|
|
123
|
+
return await (0, process_1.exec)("mysql", [
|
|
124
|
+
...(await args()),
|
|
125
|
+
`--init-command=SET ${[
|
|
126
|
+
"autocommit=0",
|
|
127
|
+
"unique_checks=0",
|
|
128
|
+
"foreign_key_checks=0",
|
|
129
|
+
].join(",")};`,
|
|
130
|
+
database,
|
|
131
|
+
], null, {
|
|
132
|
+
pipe: {
|
|
133
|
+
stream: (0, fs_2.createReadStream)(path),
|
|
134
|
+
onReadProgress: (data) => {
|
|
135
|
+
if (options.verbose)
|
|
136
|
+
(0, process_1.logExecStdout)({
|
|
137
|
+
data: JSON.stringify(data),
|
|
138
|
+
colorize: true,
|
|
139
|
+
stderr: true,
|
|
140
|
+
lineSalt: true,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
stderr: { toExitCode: true },
|
|
145
|
+
log: options.verbose,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function importCsvFile(path, database, table) {
|
|
149
|
+
return run(`
|
|
150
|
+
LOAD DATA LOCAL INFILE '${path.replaceAll("\\", "/")}'
|
|
151
|
+
INTO TABLE ${table}
|
|
152
|
+
FIELDS TERMINATED BY ','
|
|
153
|
+
ENCLOSED BY '"'
|
|
154
|
+
LINES TERMINATED BY '\\n'`, database, ["--local-infile"]);
|
|
155
|
+
}
|
|
156
|
+
async function isDatabaseEmpty(database) {
|
|
157
|
+
const [total] = await fetchAll(`
|
|
158
|
+
SELECT
|
|
159
|
+
COUNT(*) AS total
|
|
160
|
+
FROM
|
|
161
|
+
information_schema.tables
|
|
162
|
+
WHERE
|
|
163
|
+
table_schema = '${database}'
|
|
164
|
+
`);
|
|
165
|
+
return Number(total) ? false : true;
|
|
166
|
+
}
|
|
167
|
+
async function createDatabase(database) {
|
|
168
|
+
await run(`
|
|
169
|
+
CREATE DATABASE IF NOT EXISTS \`${database.name}\`
|
|
170
|
+
CHARACTER SET ${database.charset ?? "utf8"}
|
|
171
|
+
COLLATE ${database.charset ?? "utf8_general_ci"}
|
|
172
|
+
`);
|
|
173
|
+
}
|
|
174
|
+
async function fetchVariable(name) {
|
|
175
|
+
const stdout = (0, string_1.undefIfEmpty)((await run(`SHOW VARIABLES LIKE "${name}"`)).stdout.trim());
|
|
176
|
+
return stdout ? (0, string_1.undefIfEmpty)(stdout.slice(name.length).trim()) : undefined;
|
|
177
|
+
}
|
|
178
|
+
async function initSharedDir(sharedDir) {
|
|
179
|
+
const secure_file_priv = await fetchVariable("secure_file_priv");
|
|
180
|
+
if (secure_file_priv?.toUpperCase() === "NULL")
|
|
181
|
+
throw new AppError_1.AppError("'secure_file_priv' is null in MySQL Server");
|
|
182
|
+
const dir = sharedDir ??
|
|
183
|
+
secure_file_priv ??
|
|
184
|
+
(await fetchVariable("tmpdir")) ??
|
|
185
|
+
(0, os_1.tmpdir)();
|
|
186
|
+
await checkSharedDir(dir);
|
|
187
|
+
return dir;
|
|
188
|
+
}
|
|
189
|
+
async function checkSharedDir(dir) {
|
|
190
|
+
const id = (0, crypto_1.randomBytes)(8).toString("hex");
|
|
191
|
+
const outFile = (0, path_1.join)(dir, `dtt_test_${id}`);
|
|
192
|
+
const outFileVar = JSON.stringify(outFile.replaceAll("\\", "/"));
|
|
193
|
+
try {
|
|
194
|
+
await (0, fs_1.mkdirIfNotExists)(dir);
|
|
195
|
+
await (0, promises_1.chmod)(dir, 0o777);
|
|
196
|
+
await run(`SELECT 1 INTO OUTFILE ${outFileVar}`);
|
|
197
|
+
const exists = await (0, fs_1.existsFile)(outFile);
|
|
198
|
+
if (!exists)
|
|
199
|
+
throw new AppError_1.AppError(`MySQL shared dir is not reached: ${dir}`);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
try {
|
|
203
|
+
await (0, promises_1.rm)(outFile);
|
|
204
|
+
}
|
|
205
|
+
catch (e) { }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
options,
|
|
210
|
+
initSharedDir,
|
|
211
|
+
args,
|
|
212
|
+
run,
|
|
213
|
+
fetchAll,
|
|
214
|
+
dump,
|
|
215
|
+
fetchTableNames,
|
|
216
|
+
importFile,
|
|
217
|
+
isDatabaseEmpty,
|
|
218
|
+
createDatabase,
|
|
219
|
+
csvDump,
|
|
220
|
+
importCsvFile,
|
|
221
|
+
fetchVariable,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
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;
|