@datatruck/cli 0.5.0 → 0.7.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 +1 -0
- package/Action/BackupAction.js +4 -1
- package/Action/RestoreAction.d.ts +1 -0
- package/CHANGELOG.md +40 -0
- package/Command/BackupCommand.d.ts +1 -0
- package/Command/BackupCommand.js +8 -2
- package/Command/BackupSessionsCommand.js +1 -1
- package/Command/ConfigCommand.d.ts +1 -0
- package/Command/ConfigCommand.js +7 -1
- package/Command/InitCommand.js +1 -1
- package/Command/PruneCommand.js +2 -2
- package/Command/RestoreCommand.d.ts +1 -0
- package/Command/RestoreCommand.js +8 -2
- package/Command/RestoreSessionsCommand.js +1 -1
- package/Command/SnapshotsCommand.js +1 -1
- package/Repository/GitRepository.js +2 -3
- package/Repository/LocalRepository.d.ts +4 -0
- package/Repository/LocalRepository.js +38 -37
- package/Repository/RepositoryAbstract.d.ts +4 -4
- package/Repository/ResticRepository.js +30 -9
- package/SessionDriver/ConsoleSessionDriver.d.ts +6 -2
- package/SessionDriver/ConsoleSessionDriver.js +31 -40
- package/SessionDriver/SessionDriverAbstract.d.ts +1 -1
- package/SessionDriver/SessionDriverAbstract.js +1 -1
- package/SessionManager/BackupSessionManager.d.ts +1 -1
- package/SessionManager/BackupSessionManager.js +2 -2
- package/Task/GitTask.d.ts +4 -0
- package/Task/GitTask.js +28 -29
- package/Task/MariadbTask.js +27 -15
- package/Task/MssqlTask.js +1 -2
- package/Task/MysqlDumpTask.d.ts +1 -1
- package/Task/MysqlDumpTask.js +4 -2
- package/Task/PostgresqlDumpTask.d.ts +1 -1
- package/Task/PostgresqlDumpTask.js +2 -4
- package/Task/SqlDumpTaskAbstract.d.ts +1 -2
- package/Task/SqlDumpTaskAbstract.js +60 -17
- package/Task/TaskAbstract.d.ts +4 -4
- package/config.schema.json +8 -0
- package/package.json +2 -2
- package/util/GitUtil.js +2 -2
- package/util/ResticUtil.d.ts +1 -1
- package/util/ResticUtil.js +3 -4
- package/util/cli-util.d.ts +1 -0
- package/util/cli-util.js +13 -1
- package/util/datatruck/config-util.d.ts +1 -0
- package/util/datatruck/config-util.js +3 -0
- package/util/date-util.d.ts +4 -0
- package/util/date-util.js +17 -1
- package/util/fs-util.d.ts +33 -0
- package/util/fs-util.js +73 -4
- package/util/process-util.d.ts +2 -2
- package/util/process-util.js +6 -6
- package/util/string-util.d.ts +1 -0
- package/util/string-util.js +21 -1
- package/util/zip-util.d.ts +2 -2
- package/util/zip-util.js +9 -9
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ConsoleSessionDriver = void 0;
|
|
4
4
|
const AppError_1 = require("../Error/AppError");
|
|
5
5
|
const cli_util_1 = require("../util/cli-util");
|
|
6
|
+
const date_util_1 = require("../util/date-util");
|
|
6
7
|
const SessionDriverAbstract_1 = require("./SessionDriverAbstract");
|
|
7
8
|
const chalk_1 = require("chalk");
|
|
8
9
|
const sep = (0, chalk_1.grey)(`|`);
|
|
@@ -12,42 +13,28 @@ class ConsoleSessionDriver extends SessionDriverAbstract_1.SessionDriverAbstract
|
|
|
12
13
|
constructor() {
|
|
13
14
|
super(...arguments);
|
|
14
15
|
this.prints = 0;
|
|
16
|
+
this.chron = (0, date_util_1.createChron)();
|
|
15
17
|
}
|
|
16
18
|
async onInit() {
|
|
17
|
-
this.
|
|
19
|
+
this.chron.start();
|
|
18
20
|
this.renderInterval = setInterval(() => {
|
|
19
21
|
if (this.lastMessage)
|
|
20
|
-
this.printMessage(this.lastMessage);
|
|
22
|
+
this.printMessage(this.lastMessage, false);
|
|
21
23
|
}, 100);
|
|
22
24
|
}
|
|
23
|
-
async onEnd() {
|
|
25
|
+
async onEnd(data) {
|
|
24
26
|
clearInterval(this.renderInterval);
|
|
25
27
|
if (!this.options.verbose)
|
|
26
28
|
process.stdout.write(cli_util_1.showCursorCommand);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
ellapsedValue = ellapsed / 60 / 60;
|
|
32
|
-
ellapsedUnit = `hour`;
|
|
33
|
-
}
|
|
34
|
-
else if (ellapsed > 60) {
|
|
35
|
-
ellapsedValue = ellapsed / 60;
|
|
36
|
-
ellapsedUnit = `minute`;
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
ellapsedValue = ellapsed;
|
|
40
|
-
ellapsedUnit = `second`;
|
|
41
|
-
}
|
|
42
|
-
if (ellapsedValue !== 1)
|
|
43
|
-
ellapsedUnit += `s`;
|
|
44
|
-
const message = `Completed in ${ellapsedValue.toFixed(2)} ${ellapsedUnit}`;
|
|
45
|
-
console.info(`\n${(0, chalk_1.grey)(message)}`);
|
|
29
|
+
(0, cli_util_1.logVars)({
|
|
30
|
+
...data,
|
|
31
|
+
elapsed: this.chron.elapsed(true),
|
|
32
|
+
});
|
|
46
33
|
}
|
|
47
34
|
async onRead() {
|
|
48
35
|
throw new AppError_1.AppError("Method not implemented");
|
|
49
36
|
}
|
|
50
|
-
printMessage(message) {
|
|
37
|
+
printMessage(message, endMessage) {
|
|
51
38
|
const text = this.renderMessage(message);
|
|
52
39
|
if (this.options.verbose && this.lastMessageText === text) {
|
|
53
40
|
return;
|
|
@@ -58,8 +45,10 @@ class ConsoleSessionDriver extends SessionDriverAbstract_1.SessionDriverAbstract
|
|
|
58
45
|
else {
|
|
59
46
|
const columns = process.stdout.columns;
|
|
60
47
|
const line = this.renderSpinner(text);
|
|
61
|
-
const [truncatedLine,
|
|
62
|
-
|
|
48
|
+
const [truncatedLine, truncated] = endMessage
|
|
49
|
+
? [line, false]
|
|
50
|
+
: (0, cli_util_1.truncate)(line, columns);
|
|
51
|
+
if (this.lastColumns && columns !== this.lastColumns && truncated)
|
|
63
52
|
process.stdout.write(`${cli_util_1.clearCommand}\n`);
|
|
64
53
|
process.stdout.write(`${cli_util_1.clearCommand}${truncatedLine}${cli_util_1.hideCursorCommand}`);
|
|
65
54
|
this.lastColumns = columns;
|
|
@@ -82,18 +71,19 @@ class ConsoleSessionDriver extends SessionDriverAbstract_1.SessionDriverAbstract
|
|
|
82
71
|
const parts = [
|
|
83
72
|
`${padding}${message.textPrefix} [${(0, chalk_1.grey)(sessionId)}] ${message.text}`,
|
|
84
73
|
badges,
|
|
85
|
-
...(haveProgressBar
|
|
86
|
-
? [
|
|
87
|
-
(0, chalk_1.cyan)((0, cli_util_1.renderProgressBar)(message.progressPercent ?? 0, 10)),
|
|
88
|
-
`${message.progressPercent?.toFixed(2)}%`,
|
|
89
|
-
`${message.progressCurrent}/${message.progressTotal}`,
|
|
90
|
-
message.progressStep,
|
|
91
|
-
`${message.progressStepPercent
|
|
92
|
-
? (0, chalk_1.cyan)((0, cli_util_1.renderProgressBar)(message.progressStepPercent ?? 0, 10))
|
|
93
|
-
: ""}`,
|
|
94
|
-
].filter((v) => !!v?.length)
|
|
95
|
-
: []),
|
|
96
74
|
];
|
|
75
|
+
if (typeof message.progressPercent === "number") {
|
|
76
|
+
parts.push((0, chalk_1.cyan)((0, cli_util_1.renderProgressBar)(message.progressPercent ?? 0, 10)), `${message.progressPercent?.toFixed(2)}%`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof message.progressCurrent === "number" ||
|
|
79
|
+
typeof message.progressTotal === "number") {
|
|
80
|
+
parts.push(`${message.progressCurrent ?? "?"}/${message.progressTotal ?? "?"}`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof message.progressStep === "string")
|
|
83
|
+
parts.push(message.progressStep);
|
|
84
|
+
if (typeof message.progressStepPercent === "number") {
|
|
85
|
+
parts.push((0, chalk_1.cyan)((0, cli_util_1.renderProgressBar)(message.progressStepPercent ?? 0, 10)));
|
|
86
|
+
}
|
|
97
87
|
return parts.join(` ${sep} `);
|
|
98
88
|
}
|
|
99
89
|
async onWrite(data) {
|
|
@@ -169,10 +159,11 @@ class ConsoleSessionDriver extends SessionDriverAbstract_1.SessionDriverAbstract
|
|
|
169
159
|
if (isHeader && data.action === SessionDriverAbstract_1.ActionEnum.End) {
|
|
170
160
|
return;
|
|
171
161
|
}
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
162
|
+
const endMessage = !this.options.verbose &&
|
|
163
|
+
(!hasProgress || data.action === SessionDriverAbstract_1.ActionEnum.End || isHeader);
|
|
164
|
+
this.printMessage(message, endMessage);
|
|
165
|
+
if (endMessage)
|
|
166
|
+
process.stdout.write("\n");
|
|
176
167
|
}
|
|
177
168
|
}
|
|
178
169
|
exports.ConsoleSessionDriver = ConsoleSessionDriver;
|
|
@@ -73,6 +73,6 @@ export declare abstract class SessionDriverAbstract {
|
|
|
73
73
|
});
|
|
74
74
|
onInit(): Promise<void>;
|
|
75
75
|
abstract onWrite(data: WriteDataType): Promise<void>;
|
|
76
|
-
onEnd(): Promise<void>;
|
|
76
|
+
onEnd(data?: Record<string, any>): Promise<void>;
|
|
77
77
|
abstract onRead(data: ReadDataType, entity: EntityEnum): Promise<ReadResultType[]>;
|
|
78
78
|
}
|
|
@@ -27,7 +27,7 @@ export declare class BackupSessionManager {
|
|
|
27
27
|
repositoryName: string;
|
|
28
28
|
}): number;
|
|
29
29
|
initDrivers(): Promise<void>;
|
|
30
|
-
endDrivers(): Promise<void>;
|
|
30
|
+
endDrivers(data?: Record<string, any>): Promise<void>;
|
|
31
31
|
protected alter(data: WriteDataType): Promise<number>;
|
|
32
32
|
readAll(options: BackupSessionsActionOptionsType): Promise<import("../SessionDriver/SessionDriverAbstract").ReadResultType[]>;
|
|
33
33
|
init(input: Pick<BackupSessionEntity, "packageName" | "snapshotId" | "tags">): Promise<number>;
|
|
@@ -27,10 +27,10 @@ class BackupSessionManager {
|
|
|
27
27
|
await driver.onInit();
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async endDrivers() {
|
|
30
|
+
async endDrivers(data) {
|
|
31
31
|
const drivers = [this.options.driver, ...(this.options.altDrivers ?? [])];
|
|
32
32
|
for (const driver of drivers) {
|
|
33
|
-
await driver.onEnd();
|
|
33
|
+
await driver.onEnd(data);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
async alter(data) {
|
package/Task/GitTask.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare type GitTaskConfigType = {
|
|
|
18
18
|
* @default true
|
|
19
19
|
*/
|
|
20
20
|
includeConfig?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* @default 1
|
|
23
|
+
*/
|
|
24
|
+
fileCopyConcurrency?: number;
|
|
21
25
|
};
|
|
22
26
|
export declare const gitTaskName = "git";
|
|
23
27
|
export declare const gitTaskDefinition: JSONSchema7;
|
package/Task/GitTask.js
CHANGED
|
@@ -9,7 +9,6 @@ const process_util_1 = require("../util/process-util");
|
|
|
9
9
|
const TaskAbstract_1 = require("./TaskAbstract");
|
|
10
10
|
const assert_1 = require("assert");
|
|
11
11
|
const fs_1 = require("fs");
|
|
12
|
-
const fs_extra_1 = require("fs-extra");
|
|
13
12
|
const promises_1 = require("fs/promises");
|
|
14
13
|
const micromatch_1 = require("micromatch");
|
|
15
14
|
const path_1 = require("path");
|
|
@@ -49,6 +48,10 @@ exports.gitTaskDefinition = {
|
|
|
49
48
|
includeConfig: {
|
|
50
49
|
type: "boolean",
|
|
51
50
|
},
|
|
51
|
+
fileCopyConcurrency: {
|
|
52
|
+
type: "integer",
|
|
53
|
+
minimum: 1,
|
|
54
|
+
},
|
|
52
55
|
},
|
|
53
56
|
};
|
|
54
57
|
class GitTask extends TaskAbstract_1.TaskAbstract {
|
|
@@ -69,6 +72,9 @@ class GitTask extends TaskAbstract_1.TaskAbstract {
|
|
|
69
72
|
(0, assert_1.ok)(typeof targetPath === "string");
|
|
70
73
|
// Bundle
|
|
71
74
|
const bundlePath = (0, path_1.join)(targetPath, "repo.bundle");
|
|
75
|
+
await data.onProgress({
|
|
76
|
+
step: "Creating bundle...",
|
|
77
|
+
});
|
|
72
78
|
await (0, process_util_1.exec)(this.command, ["bundle", "create", bundlePath, "--all"], {
|
|
73
79
|
cwd: path,
|
|
74
80
|
}, {
|
|
@@ -152,40 +158,28 @@ class GitTask extends TaskAbstract_1.TaskAbstract {
|
|
|
152
158
|
for (const option of lsFilesConfig) {
|
|
153
159
|
if (!option.include)
|
|
154
160
|
continue;
|
|
155
|
-
const createdPaths = [];
|
|
156
161
|
const outPath = (0, path_1.join)(targetPath, `repo.${option.name}`);
|
|
157
162
|
await (0, fs_util_1.mkdirIfNotExists)(outPath);
|
|
158
163
|
if (data.options.verbose)
|
|
159
164
|
(0, cli_util_1.logExec)(`Copying ${option.name} files to ${outPath}`);
|
|
160
|
-
|
|
161
|
-
input:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
165
|
+
await (0, fs_util_1.cpy)({
|
|
166
|
+
input: {
|
|
167
|
+
type: "pathList",
|
|
168
|
+
path: option.pathsPath,
|
|
169
|
+
basePath: path,
|
|
170
|
+
},
|
|
171
|
+
targetPath: outPath,
|
|
172
|
+
concurrency: this.config.fileCopyConcurrency,
|
|
173
|
+
onPath: async ({ entryPath }) => {
|
|
172
174
|
currentFiles++;
|
|
173
175
|
await data.onProgress({
|
|
174
176
|
total,
|
|
175
177
|
current: currentFiles,
|
|
176
178
|
percent: (0, math_util_1.progressPercent)(total, currentFiles),
|
|
177
|
-
step:
|
|
179
|
+
step: entryPath,
|
|
178
180
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
await (0, promises_1.mkdir)(dir, {
|
|
182
|
-
recursive: true,
|
|
183
|
-
});
|
|
184
|
-
createdPaths.push(dir);
|
|
185
|
-
}
|
|
186
|
-
await (0, promises_1.copyFile)(source, target);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
189
183
|
await (0, promises_1.rm)(option.pathsPath);
|
|
190
184
|
}
|
|
191
185
|
}
|
|
@@ -235,10 +229,15 @@ class GitTask extends TaskAbstract_1.TaskAbstract {
|
|
|
235
229
|
if (await (0, fs_util_1.checkDir)(sourcePath)) {
|
|
236
230
|
if (data.options.verbose)
|
|
237
231
|
(0, cli_util_1.logExec)(`Copying ${name} files to ${restorePath}`);
|
|
238
|
-
await (0,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
await (0, fs_util_1.cpy)({
|
|
233
|
+
input: {
|
|
234
|
+
type: "glob",
|
|
235
|
+
sourcePath,
|
|
236
|
+
},
|
|
237
|
+
targetPath: restorePath,
|
|
238
|
+
concurrency: this.config.fileCopyConcurrency,
|
|
239
|
+
onPath: async ({ entryPath }) => {
|
|
240
|
+
await incrementProgress(entryPath);
|
|
242
241
|
},
|
|
243
242
|
});
|
|
244
243
|
}
|
package/Task/MariadbTask.js
CHANGED
|
@@ -83,21 +83,30 @@ class MariadbTask extends TaskAbstract_1.TaskAbstract {
|
|
|
83
83
|
await (0, fs_util_1.forEachFile)(sourcePath, () => {
|
|
84
84
|
total++;
|
|
85
85
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
let childProcess;
|
|
87
|
+
const onData = async (strLines) => {
|
|
88
|
+
const paths = [];
|
|
89
|
+
const pathRegex = /\[\d{1,}\] \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} Copying (.+) to/;
|
|
90
|
+
const lines = strLines.split(/\r?\n/);
|
|
91
|
+
let fatalError = false;
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
if (line.includes("[ERROR] InnoDB: Unsupported redo log format.") ||
|
|
94
|
+
line.includes("Error: cannot read redo log header")) {
|
|
95
|
+
fatalError = true;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
else {
|
|
98
|
+
const matches = pathRegex.exec(line);
|
|
99
|
+
if (matches) {
|
|
100
|
+
current++;
|
|
101
|
+
paths.push(matches[1]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (fatalError) {
|
|
106
|
+
childProcess.kill();
|
|
107
|
+
}
|
|
108
|
+
else if (paths.length) {
|
|
109
|
+
const path = (0, posix_1.normalize)(paths[0]);
|
|
101
110
|
await data.onProgress({
|
|
102
111
|
current,
|
|
103
112
|
percent: (0, math_util_1.progressPercent)(total, current),
|
|
@@ -108,6 +117,9 @@ class MariadbTask extends TaskAbstract_1.TaskAbstract {
|
|
|
108
117
|
};
|
|
109
118
|
await (0, process_util_1.exec)(command, args, undefined, {
|
|
110
119
|
log: this.verbose,
|
|
120
|
+
onSpawn: (p) => {
|
|
121
|
+
childProcess = p;
|
|
122
|
+
},
|
|
111
123
|
stdout: {
|
|
112
124
|
onData,
|
|
113
125
|
},
|
|
@@ -117,7 +129,7 @@ class MariadbTask extends TaskAbstract_1.TaskAbstract {
|
|
|
117
129
|
});
|
|
118
130
|
await (0, process_util_1.exec)(command, [`--prepare`, `--target-dir=${targetPath}`], undefined, {
|
|
119
131
|
log: this.verbose,
|
|
120
|
-
stderr: { onData:
|
|
132
|
+
stderr: { onData: () => { } },
|
|
121
133
|
});
|
|
122
134
|
}
|
|
123
135
|
async onRestore(data) {
|
package/Task/MssqlTask.js
CHANGED
|
@@ -8,7 +8,6 @@ const fs_util_1 = require("../util/fs-util");
|
|
|
8
8
|
const process_util_1 = require("../util/process-util");
|
|
9
9
|
const TaskAbstract_1 = require("./TaskAbstract");
|
|
10
10
|
const assert_1 = require("assert");
|
|
11
|
-
const fs_extra_1 = require("fs-extra");
|
|
12
11
|
const promises_1 = require("fs/promises");
|
|
13
12
|
const micromatch_1 = require("micromatch");
|
|
14
13
|
const path_1 = require("path");
|
|
@@ -85,7 +84,7 @@ class MssqlTask extends TaskAbstract_1.TaskAbstract {
|
|
|
85
84
|
const restorePath = data.package.restorePath;
|
|
86
85
|
(0, assert_1.ok)(typeof restorePath === "string");
|
|
87
86
|
await (0, fs_util_1.mkdirIfNotExists)(restorePath);
|
|
88
|
-
const files = await (0,
|
|
87
|
+
const files = await (0, promises_1.readdir)(restorePath);
|
|
89
88
|
for (const file of files) {
|
|
90
89
|
if (!file.endsWith(MssqlTask.SUFFIX))
|
|
91
90
|
continue;
|
package/Task/MysqlDumpTask.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare class MysqlDumpTask extends SqlDumpTaskAbstract<MysqlDumpTaskConf
|
|
|
8
8
|
onDatabaseIsEmpty(name: string): Promise<boolean>;
|
|
9
9
|
onCreateDatabase(database: TargetDatabaseType): Promise<void>;
|
|
10
10
|
onExecQuery(query: string): Promise<import("../util/process-util").ExecResultType>;
|
|
11
|
-
onFetchTableNames(): Promise<string[]>;
|
|
11
|
+
onFetchTableNames(database: string): Promise<string[]>;
|
|
12
12
|
onExport(tableNames: string[], output: string): Promise<void>;
|
|
13
13
|
onImport(path: string, database: string): Promise<void>;
|
|
14
14
|
}
|
package/Task/MysqlDumpTask.js
CHANGED
|
@@ -17,6 +17,7 @@ class MysqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
17
17
|
const password = await this.fetchPassword();
|
|
18
18
|
return [
|
|
19
19
|
`--host=${this.config.hostname}`,
|
|
20
|
+
...(this.config.port ? [`--port=${this.config.port}`] : []),
|
|
20
21
|
`--user=${this.config.username}`,
|
|
21
22
|
`--password=${password ?? ""}`,
|
|
22
23
|
...(database && this.config.database ? [this.config.database] : []),
|
|
@@ -57,14 +58,14 @@ class MysqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
57
58
|
},
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
|
-
async onFetchTableNames() {
|
|
61
|
+
async onFetchTableNames(database) {
|
|
61
62
|
return await this.fetchValues(`
|
|
62
63
|
SELECT
|
|
63
64
|
table_name
|
|
64
65
|
FROM
|
|
65
66
|
information_schema.tables
|
|
66
67
|
WHERE
|
|
67
|
-
table_schema = '${
|
|
68
|
+
table_schema = '${database}'
|
|
68
69
|
`);
|
|
69
70
|
}
|
|
70
71
|
async onExport(tableNames, output) {
|
|
@@ -77,6 +78,7 @@ class MysqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
77
78
|
await (0, process_util_1.exec)("mysqldump", [
|
|
78
79
|
...(await this.buildConnectionArgs()),
|
|
79
80
|
"--lock-tables=false",
|
|
81
|
+
"--skip-add-drop-table=false",
|
|
80
82
|
...tableNames,
|
|
81
83
|
], null, {
|
|
82
84
|
pipe: { stream: stream },
|
|
@@ -8,7 +8,7 @@ export declare class PostgresqlDumpTask extends SqlDumpTaskAbstract<PostgresqlDu
|
|
|
8
8
|
onDatabaseIsEmpty(name: string): Promise<boolean>;
|
|
9
9
|
onCreateDatabase(database: TargetDatabaseType): Promise<void>;
|
|
10
10
|
onExecQuery(query: string): Promise<import("../util/process-util").ExecResultType>;
|
|
11
|
-
onFetchTableNames(): Promise<string[]>;
|
|
11
|
+
onFetchTableNames(database: string): Promise<string[]>;
|
|
12
12
|
onExport(tableNames: string[], output: string): Promise<void>;
|
|
13
13
|
onImport(path: string, database: string): Promise<void>;
|
|
14
14
|
}
|
|
@@ -60,14 +60,14 @@ class PostgresqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
-
async onFetchTableNames() {
|
|
63
|
+
async onFetchTableNames(database) {
|
|
64
64
|
return await this.fetchValues(`
|
|
65
65
|
SELECT
|
|
66
66
|
CONCAT(table_schema, '.', table_name)
|
|
67
67
|
FROM
|
|
68
68
|
information_schema.tables
|
|
69
69
|
WHERE
|
|
70
|
-
table_catalog = '${
|
|
70
|
+
table_catalog = '${database}' AND
|
|
71
71
|
table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
72
72
|
`);
|
|
73
73
|
}
|
|
@@ -80,8 +80,6 @@ class PostgresqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
80
80
|
}),
|
|
81
81
|
(0, process_util_1.exec)("pg_dump", [
|
|
82
82
|
...(await this.buildConnectionArgs()),
|
|
83
|
-
"--clean",
|
|
84
|
-
"--if-exists",
|
|
85
83
|
...(tableNames?.flatMap((v) => ["-t", v]) ?? []),
|
|
86
84
|
], null, {
|
|
87
85
|
pipe: { stream: stream },
|
|
@@ -26,11 +26,10 @@ export declare abstract class SqlDumpTaskAbstract<TConfig extends SqlDumpTaskCon
|
|
|
26
26
|
fetchValues(query: string): Promise<string[]>;
|
|
27
27
|
abstract onCreateDatabase(database: TargetDatabaseType): Promise<void>;
|
|
28
28
|
abstract onDatabaseIsEmpty(databaseName: string): Promise<boolean>;
|
|
29
|
-
abstract onFetchTableNames(): Promise<string[]>;
|
|
29
|
+
abstract onFetchTableNames(database: string): Promise<string[]>;
|
|
30
30
|
abstract onExecQuery(query: string): ReturnType<typeof exec>;
|
|
31
31
|
abstract onExport(tableNames: string[], output: string): Promise<void>;
|
|
32
32
|
abstract onImport(path: string, database: string): Promise<void>;
|
|
33
|
-
fetchTableNames(): Promise<string[]>;
|
|
34
33
|
onBackup(data: BackupDataType): Promise<void>;
|
|
35
34
|
onRestore(data: RestoreDataType): Promise<void>;
|
|
36
35
|
}
|
|
@@ -49,6 +49,39 @@ exports.sqlDumpTaskDefinition = {
|
|
|
49
49
|
oneFileByTable: { type: "boolean" },
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
|
+
function serializeSqlFile(input) {
|
|
53
|
+
if (input.database && input.table) {
|
|
54
|
+
return `${input.database}.${input.table}.table.sql`;
|
|
55
|
+
}
|
|
56
|
+
else if (input.database && !input.table) {
|
|
57
|
+
return `${input.database}.database.sql`;
|
|
58
|
+
}
|
|
59
|
+
else if (!input.database && input.table) {
|
|
60
|
+
return `${input.table}.table.sql`;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new AppError_1.AppError(`Invalid sql file input: ${JSON.stringify(input)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function parseSqlFile(fileName) {
|
|
67
|
+
if (!fileName.endsWith(".sql"))
|
|
68
|
+
return;
|
|
69
|
+
const regex = /^(.+)\.(table|database)\.sql$/;
|
|
70
|
+
const matches = regex.exec(fileName);
|
|
71
|
+
if (!matches)
|
|
72
|
+
return { fileName };
|
|
73
|
+
const [, name, type] = matches;
|
|
74
|
+
const lastName = name.split(".").pop();
|
|
75
|
+
if (type === "table") {
|
|
76
|
+
return { fileName, table: lastName };
|
|
77
|
+
}
|
|
78
|
+
else if (type === "database") {
|
|
79
|
+
return { fileName, database: lastName };
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
throw new Error(`Invalid sql file type: ${type}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
52
85
|
class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
53
86
|
async fetchPassword() {
|
|
54
87
|
if (typeof this.config.password === "string")
|
|
@@ -66,26 +99,23 @@ class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
|
66
99
|
return result;
|
|
67
100
|
}, []);
|
|
68
101
|
}
|
|
69
|
-
async
|
|
70
|
-
|
|
102
|
+
async onBackup(data) {
|
|
103
|
+
this.verbose = data.options.verbose;
|
|
71
104
|
const config = this.config;
|
|
72
|
-
|
|
105
|
+
const outputPath = data.package.path;
|
|
106
|
+
const allTableNames = await this.onFetchTableNames(this.config.database);
|
|
107
|
+
const tableNames = allTableNames.filter((tableName) => {
|
|
73
108
|
if (config.includeTables && !(0, micromatch_1.isMatch)(tableName, config.includeTables))
|
|
74
109
|
return false;
|
|
75
110
|
if (config.excludeTables && (0, micromatch_1.isMatch)(tableName, config.excludeTables))
|
|
76
111
|
return false;
|
|
77
112
|
return true;
|
|
78
113
|
});
|
|
79
|
-
}
|
|
80
|
-
async onBackup(data) {
|
|
81
|
-
this.verbose = data.options.verbose;
|
|
82
|
-
const outputPath = data.package.path;
|
|
83
|
-
const tableNames = await this.fetchTableNames();
|
|
84
114
|
(0, assert_1.ok)(typeof outputPath === "string");
|
|
85
115
|
if (!(await (0, fs_util_1.checkDir)(outputPath)))
|
|
86
116
|
await (0, promises_1.mkdir)(outputPath, { recursive: true });
|
|
87
117
|
if (!this.config.oneFileByTable) {
|
|
88
|
-
const outPath = (0, path_1.join)(outputPath, this.config.database)
|
|
118
|
+
const outPath = (0, path_1.join)(outputPath, serializeSqlFile({ database: this.config.database }));
|
|
89
119
|
await this.onExport(tableNames, outPath);
|
|
90
120
|
}
|
|
91
121
|
else {
|
|
@@ -98,7 +128,7 @@ class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
|
98
128
|
step: tableName,
|
|
99
129
|
});
|
|
100
130
|
current++;
|
|
101
|
-
const outPath = (0, path_1.join)(outputPath, tableName)
|
|
131
|
+
const outPath = (0, path_1.join)(outputPath, serializeSqlFile({ table: tableName }));
|
|
102
132
|
await this.onExport([tableName], outPath);
|
|
103
133
|
}
|
|
104
134
|
}
|
|
@@ -125,20 +155,33 @@ class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
|
125
155
|
database: database.name,
|
|
126
156
|
});
|
|
127
157
|
}
|
|
128
|
-
|
|
158
|
+
const items = (await (0, promises_1.readdir)(restorePath))
|
|
159
|
+
.map(parseSqlFile)
|
|
160
|
+
.filter((v) => !!v);
|
|
161
|
+
// Database check
|
|
162
|
+
const databaseItems = items.filter((v) => v.database);
|
|
163
|
+
if (databaseItems.length && !(await this.onDatabaseIsEmpty(database.name)))
|
|
129
164
|
throw new AppError_1.AppError(`Target database is not empty: ${database.name}`);
|
|
165
|
+
// Table check
|
|
166
|
+
const restoreTables = items
|
|
167
|
+
.filter((v) => v.table)
|
|
168
|
+
.map((v) => v.table);
|
|
169
|
+
const serverTables = await this.onFetchTableNames(database.name);
|
|
170
|
+
const errorTables = restoreTables.filter((v) => serverTables.includes(v));
|
|
171
|
+
if (errorTables.length) {
|
|
172
|
+
throw new AppError_1.AppError(`Target table already exists: ${errorTables.join(", ")}`);
|
|
173
|
+
}
|
|
130
174
|
await this.onCreateDatabase(database);
|
|
131
175
|
if (this.verbose)
|
|
132
176
|
(0, cli_util_1.logExec)("readdir", [restorePath]);
|
|
133
|
-
const files = (await (0, promises_1.readdir)(restorePath)).filter((name) => /\.sql$/i.test(name));
|
|
134
177
|
let current = 0;
|
|
135
|
-
for (const
|
|
136
|
-
const path = (0, path_1.join)(restorePath,
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
const path = (0, path_1.join)(restorePath, item.fileName);
|
|
137
180
|
data.onProgress({
|
|
138
|
-
total:
|
|
181
|
+
total: items.length,
|
|
139
182
|
current: current,
|
|
140
|
-
percent: (0, math_util_1.progressPercent)(
|
|
141
|
-
step:
|
|
183
|
+
percent: (0, math_util_1.progressPercent)(items.length, current),
|
|
184
|
+
step: item.fileName,
|
|
142
185
|
});
|
|
143
186
|
current++;
|
|
144
187
|
await this.onImport(path, database.name);
|
package/Task/TaskAbstract.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { RestoreActionOptionsType } from "../Action/RestoreAction";
|
|
|
3
3
|
import { PackageConfigType } from "../Config/PackageConfig";
|
|
4
4
|
import { SnapshotType } from "../Repository/RepositoryAbstract";
|
|
5
5
|
export declare type ProgressDataType = {
|
|
6
|
-
total
|
|
7
|
-
current
|
|
8
|
-
percent
|
|
9
|
-
step
|
|
6
|
+
total?: number;
|
|
7
|
+
current?: number;
|
|
8
|
+
percent?: number;
|
|
9
|
+
step?: string;
|
|
10
10
|
stepPercent?: number;
|
|
11
11
|
};
|
|
12
12
|
export declare type BackupDataType = {
|
package/config.schema.json
CHANGED
|
@@ -487,6 +487,10 @@
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
]
|
|
490
|
+
},
|
|
491
|
+
"fileCopyConcurrency": {
|
|
492
|
+
"type": "integer",
|
|
493
|
+
"minimum": 1
|
|
490
494
|
}
|
|
491
495
|
}
|
|
492
496
|
},
|
|
@@ -615,6 +619,10 @@
|
|
|
615
619
|
},
|
|
616
620
|
"includeConfig": {
|
|
617
621
|
"type": "boolean"
|
|
622
|
+
},
|
|
623
|
+
"fileCopyConcurrency": {
|
|
624
|
+
"type": "integer",
|
|
625
|
+
"minimum": 1
|
|
618
626
|
}
|
|
619
627
|
}
|
|
620
628
|
},
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datatruck/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"ajv": "^8.11.0",
|
|
6
|
+
"async": "^3.2.4",
|
|
6
7
|
"chalk": "^4.1.2",
|
|
7
8
|
"cli-table3": "^0.6.2",
|
|
8
9
|
"commander": "^9.2.0",
|
|
9
10
|
"dayjs": "^1.11.2",
|
|
10
11
|
"fast-glob": "^3.2.11",
|
|
11
|
-
"fs-extra": "^10.1.0",
|
|
12
12
|
"micromatch": "^4.0.5",
|
|
13
13
|
"sqlite": "^4.1.1",
|
|
14
14
|
"sqlite3": "^5.0.8"
|
package/util/GitUtil.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GitUtil = void 0;
|
|
4
4
|
const fs_util_1 = require("./fs-util");
|
|
5
5
|
const process_util_1 = require("./process-util");
|
|
6
|
-
const
|
|
6
|
+
const promises_1 = require("fs/promises");
|
|
7
7
|
class GitUtil {
|
|
8
8
|
constructor(options) {
|
|
9
9
|
this.options = options;
|
|
@@ -16,7 +16,7 @@ class GitUtil {
|
|
|
16
16
|
}
|
|
17
17
|
async canBeInit(repo) {
|
|
18
18
|
return ((0, fs_util_1.isLocalDir)(repo) &&
|
|
19
|
-
(!(await (0, fs_util_1.checkDir)(repo)) || !(await (0,
|
|
19
|
+
(!(await (0, fs_util_1.checkDir)(repo)) || !(await (0, promises_1.readdir)(repo)).length));
|
|
20
20
|
}
|
|
21
21
|
async clone(options) {
|
|
22
22
|
return await this.exec([
|
package/util/ResticUtil.d.ts
CHANGED
|
@@ -84,7 +84,7 @@ export declare class ResticUtil {
|
|
|
84
84
|
excludeFile?: string[];
|
|
85
85
|
parent?: string;
|
|
86
86
|
allowEmptySnapshot?: boolean;
|
|
87
|
-
onStream?: (data: BackupStreamType) =>
|
|
87
|
+
onStream?: (data: BackupStreamType) => void;
|
|
88
88
|
}): Promise<ExecResultType>;
|
|
89
89
|
restore(options: {
|
|
90
90
|
id: string;
|