@datatruck/cli 0.7.0 → 0.10.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 +68 -1
- package/Action/ConfigAction.js +16 -0
- package/Action/RestoreAction.js +1 -0
- package/Action/SnapshotsAction.d.ts +1 -0
- package/Command/BackupSessionsCommand.js +1 -1
- package/Command/ConfigCommand.js +1 -1
- package/Command/InitCommand.js +1 -1
- package/Command/PruneCommand.js +1 -1
- package/Command/RestoreSessionsCommand.js +1 -1
- package/Command/SnapshotsCommand.d.ts +1 -0
- package/Command/SnapshotsCommand.js +16 -2
- package/Config/RepositoryConfig.d.ts +1 -0
- package/Config/RepositoryConfig.js +1 -0
- package/Factory/CommandFactory.js +2 -2
- package/Repository/GitRepository.d.ts +2 -1
- package/Repository/GitRepository.js +8 -0
- package/Repository/LocalRepository.d.ts +3 -1
- package/Repository/LocalRepository.js +28 -1
- package/Repository/RepositoryAbstract.d.ts +12 -1
- package/Repository/RepositoryAbstract.js +1 -0
- package/Repository/ResticRepository.d.ts +4 -3
- package/Repository/ResticRepository.js +33 -0
- package/Task/MariadbTask.js +1 -1
- package/Task/MssqlTask.js +1 -1
- package/Task/MysqlDumpTask.d.ts +2 -1
- package/Task/MysqlDumpTask.js +31 -1
- package/Task/PostgresqlDumpTask.d.ts +2 -1
- package/Task/PostgresqlDumpTask.js +4 -1
- package/Task/SqlDumpTaskAbstract.d.ts +3 -1
- package/Task/SqlDumpTaskAbstract.js +8 -3
- package/cli.js +1 -1
- package/config.schema.json +6 -0
- package/package.json +11 -11
- package/util/GitUtil.js +1 -2
- package/util/ResticUtil.d.ts +4 -0
- package/util/ResticUtil.js +16 -0
- package/util/cli-util.js +2 -2
- package/util/date-util.d.ts +1 -1
- package/util/entity-util.d.ts +1 -1
- package/util/fs-util.d.ts +2 -0
- package/util/fs-util.js +21 -4
- package/util/process-util.d.ts +1 -0
- package/CHANGELOG.md +0 -139
package/Action/BackupAction.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare class BackupAction<TRequired extends boolean = true> {
|
|
|
25
25
|
protected init(session: BackupSessionManager): Promise<[SnapshotType, PackageConfigType[]]>;
|
|
26
26
|
protected execTask(session: BackupSessionManager, pkg: PackageConfigType, task: TaskConfigType, snapshot: SnapshotType, targetPath: string | undefined): Promise<boolean>;
|
|
27
27
|
protected execRepository(session: BackupSessionManager, pkg: PackageConfigType, repo: RepositoryConfigType, snapshot: SnapshotType, targetPath: string | undefined): Promise<boolean>;
|
|
28
|
+
protected execCopyRepository(session: BackupSessionManager, pkg: PackageConfigType, repo: RepositoryConfigType, mirrorRepo: RepositoryConfigType, snapshot: SnapshotType): Promise<boolean>;
|
|
28
29
|
protected getError(pkg: PackageConfigType): AppError | null;
|
|
29
30
|
exec(session: BackupSessionManager): Promise<{
|
|
30
31
|
total: number;
|
package/Action/BackupAction.js
CHANGED
|
@@ -144,6 +144,50 @@ class BackupAction {
|
|
|
144
144
|
});
|
|
145
145
|
return error ? false : true;
|
|
146
146
|
}
|
|
147
|
+
async execCopyRepository(session, pkg, repo, mirrorRepo, snapshot) {
|
|
148
|
+
const repositoryId = session.findRepositoryId({
|
|
149
|
+
packageName: pkg.name,
|
|
150
|
+
repositoryName: mirrorRepo.name,
|
|
151
|
+
});
|
|
152
|
+
await session.startRepository({
|
|
153
|
+
id: repositoryId,
|
|
154
|
+
});
|
|
155
|
+
let error;
|
|
156
|
+
if (this.taskErrors[pkg.name]?.length) {
|
|
157
|
+
error = new AppError_1.AppError("Task failed");
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
try {
|
|
161
|
+
const repoInstance = (0, RepositoryFactory_1.RepositoryFactory)(repo);
|
|
162
|
+
await repoInstance.onCopyBackup({
|
|
163
|
+
options: this.options,
|
|
164
|
+
package: pkg,
|
|
165
|
+
snapshot,
|
|
166
|
+
mirrorRepositoryConfig: mirrorRepo.config,
|
|
167
|
+
onProgress: async (data) => {
|
|
168
|
+
await session.progressRepository({
|
|
169
|
+
id: repositoryId,
|
|
170
|
+
progressCurrent: data.current,
|
|
171
|
+
progressPercent: data.percent,
|
|
172
|
+
progressStep: data.step,
|
|
173
|
+
progressStepPercent: data.stepPercent,
|
|
174
|
+
progressTotal: data.total,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (_) {
|
|
180
|
+
if (!this.repoErrors[pkg.name])
|
|
181
|
+
this.repoErrors[pkg.name] = [];
|
|
182
|
+
this.repoErrors[pkg.name].push((error = _));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
await session.endRepository({
|
|
186
|
+
id: repositoryId,
|
|
187
|
+
error: error?.stack,
|
|
188
|
+
});
|
|
189
|
+
return error ? false : true;
|
|
190
|
+
}
|
|
147
191
|
getError(pkg) {
|
|
148
192
|
const taskErrors = this.taskErrors[pkg.name]?.length;
|
|
149
193
|
const repoErrors = this.repoErrors[pkg.name]?.length;
|
|
@@ -182,10 +226,33 @@ class BackupAction {
|
|
|
182
226
|
});
|
|
183
227
|
await this.execTask(session, pkg, pkg.task, snapshot, (targetPath = result?.targetPath));
|
|
184
228
|
}
|
|
185
|
-
|
|
229
|
+
const mirrorRepoMap = {};
|
|
230
|
+
const allMirrorRepoNames = [];
|
|
231
|
+
const repoNames = pkg.repositoryNames ?? [];
|
|
232
|
+
for (const repoName of repoNames) {
|
|
233
|
+
const repo = (0, config_util_1.findRepositoryOrFail)(this.config, repoName);
|
|
234
|
+
if (repo.mirrorRepoNames)
|
|
235
|
+
mirrorRepoMap[repoName] = repo.mirrorRepoNames.filter((mirrorRepoName) => {
|
|
236
|
+
allMirrorRepoNames.push(mirrorRepoName);
|
|
237
|
+
return repoNames.includes(mirrorRepoName);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
for (const repoName of repoNames) {
|
|
241
|
+
if (allMirrorRepoNames.includes(repoName))
|
|
242
|
+
continue;
|
|
186
243
|
const repo = (0, config_util_1.findRepositoryOrFail)(this.config, repoName);
|
|
187
244
|
await this.execRepository(session, pkg, repo, snapshot, targetPath);
|
|
188
245
|
}
|
|
246
|
+
for (const repoName of repoNames) {
|
|
247
|
+
const repo = (0, config_util_1.findRepositoryOrFail)(this.config, repoName);
|
|
248
|
+
const mirrorRepoNames = mirrorRepoMap[repoName];
|
|
249
|
+
if (mirrorRepoNames) {
|
|
250
|
+
for (const mirrorRepoName of mirrorRepoNames) {
|
|
251
|
+
const mirrorRepo = (0, config_util_1.findRepositoryOrFail)(this.config, mirrorRepoName);
|
|
252
|
+
await this.execCopyRepository(session, pkg, repo, mirrorRepo, snapshot);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
189
256
|
const error = this.getError(pkg);
|
|
190
257
|
if (error)
|
|
191
258
|
errors++;
|
package/Action/ConfigAction.js
CHANGED
|
@@ -22,11 +22,27 @@ class ConfigAction {
|
|
|
22
22
|
}
|
|
23
23
|
static check(config) {
|
|
24
24
|
const repositoryNames = [];
|
|
25
|
+
const mirrorRepoNames = [];
|
|
26
|
+
const repos = {};
|
|
25
27
|
for (const repo of config.repositories) {
|
|
28
|
+
repos[repo.name] = repo;
|
|
26
29
|
if (repositoryNames.includes(repo.name))
|
|
27
30
|
throw new AppError_1.AppError(`Duplicated repository name: ${repo.name}`);
|
|
28
31
|
repositoryNames.push(repo.name);
|
|
29
32
|
}
|
|
33
|
+
for (const repo of config.repositories) {
|
|
34
|
+
if (repo.mirrorRepoNames) {
|
|
35
|
+
for (const mirrorRepoName of repo.mirrorRepoNames) {
|
|
36
|
+
if (!repos[mirrorRepoName])
|
|
37
|
+
throw new AppError_1.AppError(`Mirror repository name not found: ${mirrorRepoName}`);
|
|
38
|
+
if (repos[mirrorRepoName].type !== repo.type)
|
|
39
|
+
throw new AppError_1.AppError(`Mirror repository type is incompatible: ${mirrorRepoName}`);
|
|
40
|
+
if (mirrorRepoNames.includes(mirrorRepoName))
|
|
41
|
+
throw new AppError_1.AppError(`Mirror repository is already used`);
|
|
42
|
+
mirrorRepoNames.push(mirrorRepoName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
30
46
|
const packageNames = [];
|
|
31
47
|
for (const pkg of config.packages) {
|
|
32
48
|
if (packageNames.includes(pkg.name))
|
package/Action/RestoreAction.js
CHANGED
|
@@ -77,7 +77,7 @@ class BackupSessionsCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
79
|
if (this.globalOptions.outputFormat)
|
|
80
|
-
console.
|
|
80
|
+
console.info(dataFormat.format(this.globalOptions.outputFormat));
|
|
81
81
|
return 0;
|
|
82
82
|
}
|
|
83
83
|
}
|
package/Command/ConfigCommand.js
CHANGED
|
@@ -56,7 +56,7 @@ class ConfigCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
if (this.globalOptions.outputFormat)
|
|
59
|
-
console.
|
|
59
|
+
console.info(dataFormat.format(this.globalOptions.outputFormat));
|
|
60
60
|
return 0;
|
|
61
61
|
}
|
|
62
62
|
}
|
package/Command/InitCommand.js
CHANGED
|
@@ -56,7 +56,7 @@ class InitCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
if (this.globalOptions.outputFormat)
|
|
59
|
-
console.
|
|
59
|
+
console.info(dataFormat.format(this.globalOptions.outputFormat));
|
|
60
60
|
return 0;
|
|
61
61
|
}
|
|
62
62
|
}
|
package/Command/PruneCommand.js
CHANGED
|
@@ -144,7 +144,7 @@ class PruneCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
144
144
|
},
|
|
145
145
|
});
|
|
146
146
|
if (this.globalOptions.outputFormat)
|
|
147
|
-
console.
|
|
147
|
+
console.info(dataFormat.format(this.globalOptions.outputFormat));
|
|
148
148
|
if (!this.options.confirm && !this.options.dryRun) {
|
|
149
149
|
const answer = await (0, cli_util_1.confirm)(`Delete ${pruneResult.prune}/${pruneResult.total} snapshots?`);
|
|
150
150
|
if (answer)
|
|
@@ -76,7 +76,7 @@ class RestoreSessionsCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
if (this.globalOptions.outputFormat)
|
|
79
|
-
console.
|
|
79
|
+
console.info(dataFormat.format(this.globalOptions.outputFormat));
|
|
80
80
|
return 0;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -5,6 +5,7 @@ import { CommandAbstract } from "./CommandAbstract";
|
|
|
5
5
|
export declare type SnapshotsCommandOptionsType<TResolved = false> = {
|
|
6
6
|
id?: If<TResolved, string[]>;
|
|
7
7
|
package?: If<TResolved, string[]>;
|
|
8
|
+
packageTask?: If<TResolved, string[]>;
|
|
8
9
|
repository?: If<TResolved, string[]>;
|
|
9
10
|
repositoryType?: If<TResolved, RepositoryConfigType["type"][]>;
|
|
10
11
|
longId?: boolean;
|
|
@@ -64,6 +64,11 @@ class SnapshotsCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
64
64
|
description: "Package names",
|
|
65
65
|
parser: string_util_1.parseStringList,
|
|
66
66
|
},
|
|
67
|
+
packageTask: {
|
|
68
|
+
description: "Package task names",
|
|
69
|
+
option: "-pt,--package-task <values>",
|
|
70
|
+
parser: string_util_1.parseStringList,
|
|
71
|
+
},
|
|
67
72
|
repository: {
|
|
68
73
|
option: "-r,--repository <names>",
|
|
69
74
|
description: "Repository names",
|
|
@@ -87,6 +92,7 @@ class SnapshotsCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
87
92
|
const snapshots = new SnapshotsAction_1.SnapshotsAction(config, {
|
|
88
93
|
ids: this.options.id,
|
|
89
94
|
packageNames: this.options.package,
|
|
95
|
+
packageTaskNames: this.options.packageTask,
|
|
90
96
|
repositoryNames: this.options.repository,
|
|
91
97
|
repositoryTypes: this.options.repositoryType,
|
|
92
98
|
last: this.options.last,
|
|
@@ -103,18 +109,26 @@ class SnapshotsCommand extends CommandAbstract_1.CommandAbstract {
|
|
|
103
109
|
const dataFormat = new DataFormat_1.DataFormat({
|
|
104
110
|
items: await snapshots.exec(),
|
|
105
111
|
table: {
|
|
106
|
-
labels: [
|
|
112
|
+
labels: [
|
|
113
|
+
"Id.",
|
|
114
|
+
"Date",
|
|
115
|
+
"Package",
|
|
116
|
+
"Task",
|
|
117
|
+
"Repository",
|
|
118
|
+
"Repository type",
|
|
119
|
+
],
|
|
107
120
|
handler: (item) => [
|
|
108
121
|
this.options.longId ? item.id : item.id.slice(0, 8),
|
|
109
122
|
item.date.replace("T", " ").replace("Z", ""),
|
|
110
123
|
item.packageName,
|
|
124
|
+
item.packageTaskName || "",
|
|
111
125
|
item.repositoryName,
|
|
112
126
|
item.repositoryType,
|
|
113
127
|
],
|
|
114
128
|
},
|
|
115
129
|
});
|
|
116
130
|
if (this.globalOptions.outputFormat)
|
|
117
|
-
console.
|
|
131
|
+
console.info(dataFormat.format(this.globalOptions.outputFormat));
|
|
118
132
|
return 0;
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -7,6 +7,7 @@ export declare type RepositoryConfigTypeType = RepositoryConfigType["type"];
|
|
|
7
7
|
export declare type RepositoryConfigEnabledActionType = "backup" | "init" | "prune" | "restore" | "snapshots";
|
|
8
8
|
export declare type RepositoryConfigType = {
|
|
9
9
|
name: string;
|
|
10
|
+
mirrorRepoNames?: string[];
|
|
10
11
|
enabled?: boolean | {
|
|
11
12
|
[K in "defaults" | RepositoryConfigEnabledActionType]?: boolean;
|
|
12
13
|
};
|
|
@@ -35,11 +35,11 @@ exports.exec = exec;
|
|
|
35
35
|
function makeParseLog(type) {
|
|
36
36
|
const data = [];
|
|
37
37
|
const consoleLog = console.log;
|
|
38
|
-
console.log = (...items) => {
|
|
38
|
+
console.log = console.info = (...items) => {
|
|
39
39
|
data.push(...items);
|
|
40
40
|
};
|
|
41
41
|
return function parseLog() {
|
|
42
|
-
console.log = consoleLog;
|
|
42
|
+
console.log = console.info = consoleLog;
|
|
43
43
|
return JSON.parse(data.flat().join("\n"));
|
|
44
44
|
};
|
|
45
45
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RepositoryAbstract, BackupDataType, InitDataType, RestoreDataType, SnapshotsDataType, SnapshotResultType, SnapshotTagEnum, SnapshotTagObjectType, PruneDataType } from "./RepositoryAbstract";
|
|
1
|
+
import { RepositoryAbstract, BackupDataType, InitDataType, RestoreDataType, SnapshotsDataType, SnapshotResultType, SnapshotTagEnum, SnapshotTagObjectType, PruneDataType, CopyBackupType } from "./RepositoryAbstract";
|
|
2
2
|
import { JSONSchema7 } from "json-schema";
|
|
3
3
|
export declare type GitRepositoryConfigType = {
|
|
4
4
|
repo: string;
|
|
@@ -25,5 +25,6 @@ export declare class GitRepository extends RepositoryAbstract<GitRepositoryConfi
|
|
|
25
25
|
onPrune(data: PruneDataType): Promise<void>;
|
|
26
26
|
onSnapshots(data: SnapshotsDataType): Promise<SnapshotResultType[]>;
|
|
27
27
|
onBackup(data: BackupDataType<GitPackageRepositoryConfigType>): Promise<void>;
|
|
28
|
+
onCopyBackup(data: CopyBackupType<GitRepositoryConfigType>): Promise<void>;
|
|
28
29
|
onRestore(data: RestoreDataType<GitPackageRepositoryConfigType>): Promise<void>;
|
|
29
30
|
}
|
|
@@ -111,6 +111,7 @@ class GitRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
111
111
|
log: data.options.verbose,
|
|
112
112
|
});
|
|
113
113
|
const pkgPatterns = (0, string_util_1.makePathPatterns)(data.options.packageNames);
|
|
114
|
+
const pkgTaskPatterns = (0, string_util_1.makePathPatterns)(data.options.packageTaskNames);
|
|
114
115
|
await git.clone({ repo: this.config.repo });
|
|
115
116
|
const tagNames = data.options.ids?.map((id) => `${GitRepository.refPrefix}/*/${id}*`) || [`${GitRepository.refPrefix}/*`];
|
|
116
117
|
const tags = await git.getTags(tagNames);
|
|
@@ -123,6 +124,8 @@ class GitRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
123
124
|
return result;
|
|
124
125
|
if (pkgPatterns && !(0, micromatch_1.isMatch)(parsedTag.package, pkgPatterns))
|
|
125
126
|
return result;
|
|
127
|
+
if (pkgTaskPatterns && !(0, micromatch_1.isMatch)(parsedTag.task || "", pkgTaskPatterns))
|
|
128
|
+
return result;
|
|
126
129
|
if (data.options.tags &&
|
|
127
130
|
!parsedTag.tags.some((value) => data.options.tags?.includes(value)))
|
|
128
131
|
return result;
|
|
@@ -131,6 +134,7 @@ class GitRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
131
134
|
id: parsedTag.id,
|
|
132
135
|
date: parsedTag.date,
|
|
133
136
|
packageName: parsedTag.package,
|
|
137
|
+
packageTaskName: parsedTag.task,
|
|
134
138
|
tags: parsedTag.tags,
|
|
135
139
|
});
|
|
136
140
|
return result;
|
|
@@ -201,6 +205,7 @@ class GitRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
201
205
|
tags: data.options.tags ?? [],
|
|
202
206
|
date: data.snapshot.date,
|
|
203
207
|
package: data.package.name,
|
|
208
|
+
task: data.package.task?.name,
|
|
204
209
|
version: nodePkg.version,
|
|
205
210
|
});
|
|
206
211
|
await git.addTag(meta.name, meta.message);
|
|
@@ -210,6 +215,9 @@ class GitRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
210
215
|
recursive: true,
|
|
211
216
|
});
|
|
212
217
|
}
|
|
218
|
+
onCopyBackup(data) {
|
|
219
|
+
throw new Error("Method not implemented.");
|
|
220
|
+
}
|
|
213
221
|
async onRestore(data) {
|
|
214
222
|
const restorePath = data.targetPath ?? data.package.restorePath;
|
|
215
223
|
(0, assert_1.ok)(restorePath);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { RepositoryAbstract, BackupDataType, InitDataType, RestoreDataType, SnapshotsDataType, SnapshotResultType, PruneDataType } from "./RepositoryAbstract";
|
|
1
|
+
import { RepositoryAbstract, BackupDataType, InitDataType, RestoreDataType, SnapshotsDataType, SnapshotResultType, PruneDataType, CopyBackupType } from "./RepositoryAbstract";
|
|
2
2
|
import type { JSONSchema7 } from "json-schema";
|
|
3
3
|
export declare type MetaDataType = {
|
|
4
4
|
id: string;
|
|
5
5
|
date: string;
|
|
6
6
|
package: string;
|
|
7
|
+
task: string | undefined;
|
|
7
8
|
tags: string[];
|
|
8
9
|
version: string;
|
|
9
10
|
};
|
|
@@ -50,6 +51,7 @@ export declare class LocalRepository extends RepositoryAbstract<LocalRepositoryC
|
|
|
50
51
|
onSnapshots(data: SnapshotsDataType): Promise<SnapshotResultType[]>;
|
|
51
52
|
private normalizeCompressConfig;
|
|
52
53
|
onBackup(data: BackupDataType<LocalPackageRepositoryConfigType>): Promise<void>;
|
|
54
|
+
onCopyBackup(data: CopyBackupType<LocalRepositoryConfigType>): Promise<void>;
|
|
53
55
|
onRestore(data: RestoreDataType<LocalPackageRepositoryConfigType>): Promise<void>;
|
|
54
56
|
}
|
|
55
57
|
export {};
|
|
@@ -122,9 +122,10 @@ class LocalRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
122
122
|
async onSnapshots(data) {
|
|
123
123
|
if (!(await (0, fs_util_1.checkDir)(this.config.outPath)))
|
|
124
124
|
throw new Error(`Repository (${this.repository.name}) out path does not exist: ${this.config.outPath}`);
|
|
125
|
-
const snapshotNames = await (0,
|
|
125
|
+
const snapshotNames = await (0, fs_util_1.readDir)(this.config.outPath);
|
|
126
126
|
const snapshots = [];
|
|
127
127
|
const packagePatterns = (0, string_util_1.makePathPatterns)(data.options.packageNames);
|
|
128
|
+
const taskPatterns = (0, string_util_1.makePathPatterns)(data.options.packageTaskNames);
|
|
128
129
|
for (const snapshotName of snapshotNames) {
|
|
129
130
|
const snapshotNameData = LocalRepository.parseSnapshotName(snapshotName);
|
|
130
131
|
if (!snapshotNameData)
|
|
@@ -137,6 +138,8 @@ class LocalRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
137
138
|
continue;
|
|
138
139
|
const metaPath = (0, path_1.join)(this.config.outPath, snapshotName);
|
|
139
140
|
const meta = await LocalRepository.parseMetaData(metaPath);
|
|
141
|
+
if (taskPatterns && !(0, micromatch_1.isMatch)(meta.task || "", taskPatterns))
|
|
142
|
+
continue;
|
|
140
143
|
if (data.options.ids &&
|
|
141
144
|
!data.options.ids.some((id) => meta.id.startsWith(id)))
|
|
142
145
|
continue;
|
|
@@ -148,6 +151,7 @@ class LocalRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
148
151
|
id: meta.id,
|
|
149
152
|
date: meta.date,
|
|
150
153
|
packageName: meta.package,
|
|
154
|
+
packageTaskName: meta.task,
|
|
151
155
|
tags: meta.tags,
|
|
152
156
|
});
|
|
153
157
|
}
|
|
@@ -288,12 +292,35 @@ class LocalRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
288
292
|
date: data.snapshot.date,
|
|
289
293
|
tags: data.options.tags ?? [],
|
|
290
294
|
package: data.package.name,
|
|
295
|
+
task: data.package.task?.name,
|
|
291
296
|
version: nodePkg.version,
|
|
292
297
|
};
|
|
293
298
|
if (data.options.verbose)
|
|
294
299
|
(0, cli_util_1.logExec)(`Writing metadata into ${metaPath}`);
|
|
295
300
|
await (0, promises_1.writeFile)(metaPath, LocalRepository.stringifyMetaData(meta));
|
|
296
301
|
}
|
|
302
|
+
async onCopyBackup(data) {
|
|
303
|
+
const snapshotName = LocalRepository.buildSnapshotName({
|
|
304
|
+
snapshotId: data.snapshot.id,
|
|
305
|
+
snapshotDate: data.snapshot.date,
|
|
306
|
+
packageName: data.package.name,
|
|
307
|
+
});
|
|
308
|
+
const sourcePath = (0, path_1.resolve)((0, path_1.join)(this.config.outPath, snapshotName));
|
|
309
|
+
const targetPath = (0, path_1.resolve)((0, path_1.join)(data.mirrorRepositoryConfig.outPath, snapshotName));
|
|
310
|
+
const sourceMetaPath = `${sourcePath}.json`;
|
|
311
|
+
const targetMetaPath = `${targetPath}.json`;
|
|
312
|
+
if (data.options.verbose)
|
|
313
|
+
(0, cli_util_1.logExec)(`Copying files to ${targetPath}`);
|
|
314
|
+
await (0, promises_1.mkdir)(targetPath);
|
|
315
|
+
await (0, fs_util_1.cpy)({
|
|
316
|
+
input: {
|
|
317
|
+
type: "glob",
|
|
318
|
+
sourcePath,
|
|
319
|
+
},
|
|
320
|
+
targetPath,
|
|
321
|
+
});
|
|
322
|
+
await (0, promises_1.copyFile)(sourceMetaPath, targetMetaPath);
|
|
323
|
+
}
|
|
297
324
|
async onRestore(data) {
|
|
298
325
|
const relRestorePath = data.targetPath ?? data.package.restorePath;
|
|
299
326
|
(0, assert_1.ok)(relRestorePath);
|
|
@@ -11,6 +11,7 @@ export declare type SnapshotType = {
|
|
|
11
11
|
export declare type SnapshotResultType = SnapshotType & {
|
|
12
12
|
originalId: string;
|
|
13
13
|
packageName: string;
|
|
14
|
+
packageTaskName: string | undefined;
|
|
14
15
|
tags: string[];
|
|
15
16
|
};
|
|
16
17
|
export declare type ProgressDataType = {
|
|
@@ -24,7 +25,14 @@ export declare type InitDataType = {
|
|
|
24
25
|
options: InitActionOptionsType;
|
|
25
26
|
};
|
|
26
27
|
export declare type SnapshotsDataType = {
|
|
27
|
-
options: Pick<SnapshotsActionOptionsType, "ids" | "packageNames" | "verbose" | "tags">;
|
|
28
|
+
options: Pick<SnapshotsActionOptionsType, "ids" | "packageNames" | "packageTaskNames" | "verbose" | "tags">;
|
|
29
|
+
};
|
|
30
|
+
export declare type CopyBackupType<TRepositoryConfig> = {
|
|
31
|
+
options: BackupActionOptionsType;
|
|
32
|
+
snapshot: SnapshotType;
|
|
33
|
+
package: PackageConfigType;
|
|
34
|
+
mirrorRepositoryConfig: TRepositoryConfig;
|
|
35
|
+
onProgress: (data: ProgressDataType) => Promise<void>;
|
|
28
36
|
};
|
|
29
37
|
export declare type BackupDataType<TPackageConfig> = {
|
|
30
38
|
options: BackupActionOptionsType;
|
|
@@ -53,6 +61,7 @@ export declare enum SnapshotTagEnum {
|
|
|
53
61
|
SHORT_ID = "shortId",
|
|
54
62
|
DATE = "date",
|
|
55
63
|
PACKAGE = "package",
|
|
64
|
+
TASK = "task",
|
|
56
65
|
TAGS = "tags",
|
|
57
66
|
VERSION = "version"
|
|
58
67
|
}
|
|
@@ -61,6 +70,7 @@ export declare type SnapshotTagObjectType = {
|
|
|
61
70
|
[SnapshotTagEnum.SHORT_ID]: string;
|
|
62
71
|
[SnapshotTagEnum.DATE]: string;
|
|
63
72
|
[SnapshotTagEnum.PACKAGE]: string;
|
|
73
|
+
[SnapshotTagEnum.TASK]: string | undefined;
|
|
64
74
|
[SnapshotTagEnum.TAGS]: string[];
|
|
65
75
|
[SnapshotTagEnum.VERSION]: string;
|
|
66
76
|
};
|
|
@@ -72,6 +82,7 @@ export declare abstract class RepositoryAbstract<TConfig> {
|
|
|
72
82
|
abstract onInit(data: InitDataType): Promise<void>;
|
|
73
83
|
abstract onPrune(data: PruneDataType): Promise<void>;
|
|
74
84
|
abstract onSnapshots(data: SnapshotsDataType): Promise<SnapshotResultType[]>;
|
|
85
|
+
abstract onCopyBackup(data: CopyBackupType<TConfig>): Promise<void>;
|
|
75
86
|
abstract onBackup(data: BackupDataType<unknown>): Promise<void>;
|
|
76
87
|
abstract onRestore(data: RestoreDataType<unknown>): Promise<void>;
|
|
77
88
|
}
|
|
@@ -7,6 +7,7 @@ var SnapshotTagEnum;
|
|
|
7
7
|
SnapshotTagEnum["SHORT_ID"] = "shortId";
|
|
8
8
|
SnapshotTagEnum["DATE"] = "date";
|
|
9
9
|
SnapshotTagEnum["PACKAGE"] = "package";
|
|
10
|
+
SnapshotTagEnum["TASK"] = "task";
|
|
10
11
|
SnapshotTagEnum["TAGS"] = "tags";
|
|
11
12
|
SnapshotTagEnum["VERSION"] = "version";
|
|
12
13
|
})(SnapshotTagEnum = exports.SnapshotTagEnum || (exports.SnapshotTagEnum = {}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RepositoryType } from "../util/ResticUtil";
|
|
2
|
-
import { RepositoryAbstract, BackupDataType, InitDataType, RestoreDataType, SnapshotsDataType, SnapshotResultType, SnapshotTagObjectType, SnapshotTagEnum, PruneDataType } from "./RepositoryAbstract";
|
|
2
|
+
import { RepositoryAbstract, BackupDataType, InitDataType, RestoreDataType, SnapshotsDataType, SnapshotResultType, SnapshotTagObjectType, SnapshotTagEnum, PruneDataType, CopyBackupType } from "./RepositoryAbstract";
|
|
3
3
|
import { JSONSchema7 } from "json-schema";
|
|
4
4
|
export declare type ResticRepositoryConfigType = {
|
|
5
5
|
password: string | {
|
|
@@ -28,13 +28,14 @@ export declare class ResticRepository extends RepositoryAbstract<ResticRepositor
|
|
|
28
28
|
name: SnapshotTagEnum;
|
|
29
29
|
value: string;
|
|
30
30
|
} | null;
|
|
31
|
-
static parseSnapshotTags(tags: string[]):
|
|
31
|
+
static parseSnapshotTags(tags: string[]): SnapshotTagObjectType & {
|
|
32
32
|
tags: string[];
|
|
33
|
-
}
|
|
33
|
+
};
|
|
34
34
|
onGetSource(): string;
|
|
35
35
|
onInit(data: InitDataType): Promise<void>;
|
|
36
36
|
onSnapshots(data: SnapshotsDataType): Promise<SnapshotResultType[]>;
|
|
37
37
|
onPrune(data: PruneDataType): Promise<void>;
|
|
38
38
|
onBackup(data: BackupDataType<ResticPackageRepositoryConfigType>): Promise<void>;
|
|
39
|
+
onCopyBackup(data: CopyBackupType<ResticRepositoryConfigType>): Promise<void>;
|
|
39
40
|
onRestore(data: RestoreDataType<ResticPackageRepositoryConfigType>): Promise<void>;
|
|
40
41
|
}
|
|
@@ -126,6 +126,7 @@ class ResticRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
126
126
|
log: data.options.verbose,
|
|
127
127
|
});
|
|
128
128
|
const packagePatterns = (0, string_util_1.makePathPatterns)(data.options.packageNames);
|
|
129
|
+
const taskNamePatterns = (0, string_util_1.makePathPatterns)(data.options.packageTaskNames);
|
|
129
130
|
const result = await restic.snapshots({
|
|
130
131
|
json: true,
|
|
131
132
|
tags: [
|
|
@@ -138,12 +139,15 @@ class ResticRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
138
139
|
return items;
|
|
139
140
|
if (packagePatterns && !(0, micromatch_1.isMatch)(tag.package, packagePatterns))
|
|
140
141
|
return items;
|
|
142
|
+
if (taskNamePatterns && !(0, micromatch_1.isMatch)(tag.task || "", taskNamePatterns))
|
|
143
|
+
return items;
|
|
141
144
|
const itemTags = tag.tags ?? [];
|
|
142
145
|
if (data.options.tags && !itemTags.some((t) => itemTags.includes(t)))
|
|
143
146
|
return items;
|
|
144
147
|
items.push({
|
|
145
148
|
originalId: item.id,
|
|
146
149
|
packageName: tag.package,
|
|
150
|
+
packageTaskName: tag.task,
|
|
147
151
|
date: tag.date,
|
|
148
152
|
id: tag.id,
|
|
149
153
|
tags: itemTags,
|
|
@@ -231,6 +235,11 @@ class ResticRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
231
235
|
ResticRepository.buildSnapshotTag(RepositoryAbstract_1.SnapshotTagEnum.DATE, data.snapshot.date),
|
|
232
236
|
ResticRepository.buildSnapshotTag(RepositoryAbstract_1.SnapshotTagEnum.VERSION, nodePkg.version),
|
|
233
237
|
packageTag,
|
|
238
|
+
...(data.package.task?.name
|
|
239
|
+
? [
|
|
240
|
+
ResticRepository.buildSnapshotTag(RepositoryAbstract_1.SnapshotTagEnum.TASK, data.package.task?.name),
|
|
241
|
+
]
|
|
242
|
+
: []),
|
|
234
243
|
...(data.options.tags ?? []),
|
|
235
244
|
],
|
|
236
245
|
onStream: async (streamData) => {
|
|
@@ -262,6 +271,30 @@ class ResticRepository extends RepositoryAbstract_1.RepositoryAbstract {
|
|
|
262
271
|
percent: 100,
|
|
263
272
|
});
|
|
264
273
|
}
|
|
274
|
+
async onCopyBackup(data) {
|
|
275
|
+
const config = data.mirrorRepositoryConfig;
|
|
276
|
+
const [snapshot] = await this.onSnapshots({
|
|
277
|
+
options: {
|
|
278
|
+
ids: [data.snapshot.id],
|
|
279
|
+
packageNames: [data.package.name],
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
if (!snapshot)
|
|
283
|
+
throw new AppError_1.AppError(`Snapshot not found`);
|
|
284
|
+
const restic = new ResticUtil_1.ResticUtil({
|
|
285
|
+
env: {
|
|
286
|
+
...(await this.buildEnv()),
|
|
287
|
+
...(typeof config.password === "string"
|
|
288
|
+
? { RESTIC_PASSWORD2: config.password }
|
|
289
|
+
: { RESTIC_PASSWORD_FILE2: (0, path_1.resolve)(config.password.path) }),
|
|
290
|
+
RESTIC_REPOSITORY2: await ResticUtil_1.ResticUtil.formatRepository(config.repository),
|
|
291
|
+
},
|
|
292
|
+
log: data.options.verbose,
|
|
293
|
+
});
|
|
294
|
+
await restic.copy({
|
|
295
|
+
id: snapshot.originalId,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
265
298
|
async onRestore(data) {
|
|
266
299
|
const restorePath = data.targetPath ?? data.package.restorePath;
|
|
267
300
|
(0, assert_1.ok)(restorePath);
|
package/Task/MariadbTask.js
CHANGED
|
@@ -137,7 +137,7 @@ class MariadbTask extends TaskAbstract_1.TaskAbstract {
|
|
|
137
137
|
const restorePath = data.package.restorePath;
|
|
138
138
|
(0, assert_1.ok)(typeof restorePath === "string");
|
|
139
139
|
await (0, fs_util_1.mkdirIfNotExists)(restorePath);
|
|
140
|
-
const files = await (0,
|
|
140
|
+
const files = await (0, fs_util_1.readDir)(restorePath);
|
|
141
141
|
for (const file of files) {
|
|
142
142
|
if (file.startsWith("ib_logfile")) {
|
|
143
143
|
const filePath = (0, path_1.join)(restorePath, file);
|
package/Task/MssqlTask.js
CHANGED
|
@@ -84,7 +84,7 @@ class MssqlTask extends TaskAbstract_1.TaskAbstract {
|
|
|
84
84
|
const restorePath = data.package.restorePath;
|
|
85
85
|
(0, assert_1.ok)(typeof restorePath === "string");
|
|
86
86
|
await (0, fs_util_1.mkdirIfNotExists)(restorePath);
|
|
87
|
-
const files = await (0,
|
|
87
|
+
const files = await (0, fs_util_1.readDir)(restorePath);
|
|
88
88
|
for (const file of files) {
|
|
89
89
|
if (!file.endsWith(MssqlTask.SUFFIX))
|
|
90
90
|
continue;
|
package/Task/MysqlDumpTask.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class MysqlDumpTask extends SqlDumpTaskAbstract<MysqlDumpTaskConf
|
|
|
9
9
|
onCreateDatabase(database: TargetDatabaseType): Promise<void>;
|
|
10
10
|
onExecQuery(query: string): Promise<import("../util/process-util").ExecResultType>;
|
|
11
11
|
onFetchTableNames(database: string): Promise<string[]>;
|
|
12
|
-
|
|
12
|
+
onExportTables(tableNames: string[], output: string): Promise<void>;
|
|
13
|
+
onExportStoredPrograms(output: string): Promise<void>;
|
|
13
14
|
onImport(path: string, database: string): Promise<void>;
|
|
14
15
|
}
|
package/Task/MysqlDumpTask.js
CHANGED
|
@@ -68,7 +68,7 @@ class MysqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
68
68
|
table_schema = '${database}'
|
|
69
69
|
`);
|
|
70
70
|
}
|
|
71
|
-
async
|
|
71
|
+
async onExportTables(tableNames, output) {
|
|
72
72
|
const stream = (0, fs_1.createWriteStream)(output);
|
|
73
73
|
await Promise.all([
|
|
74
74
|
new Promise((resolve, reject) => {
|
|
@@ -107,6 +107,36 @@ class MysqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
107
107
|
if (!successFooter)
|
|
108
108
|
throw new AppError_1.AppError("No end line found (incomplete backup)");
|
|
109
109
|
}
|
|
110
|
+
async onExportStoredPrograms(output) {
|
|
111
|
+
const stream = (0, fs_1.createWriteStream)(output);
|
|
112
|
+
await Promise.all([
|
|
113
|
+
new Promise((resolve, reject) => {
|
|
114
|
+
stream.on("close", resolve);
|
|
115
|
+
stream.on("error", reject);
|
|
116
|
+
}),
|
|
117
|
+
await (0, process_util_1.exec)("mysqldump", [
|
|
118
|
+
...(await this.buildConnectionArgs()),
|
|
119
|
+
"--lock-tables=false",
|
|
120
|
+
"--routines",
|
|
121
|
+
"--events",
|
|
122
|
+
"--skip-triggers",
|
|
123
|
+
"--no-create-info",
|
|
124
|
+
"--no-data",
|
|
125
|
+
"--no-create-db",
|
|
126
|
+
"--skip-opt",
|
|
127
|
+
], null, {
|
|
128
|
+
pipe: { stream: stream },
|
|
129
|
+
log: {
|
|
130
|
+
exec: this.verbose,
|
|
131
|
+
stderr: this.verbose,
|
|
132
|
+
allToStderr: true,
|
|
133
|
+
},
|
|
134
|
+
stderr: {
|
|
135
|
+
toExitCode: true,
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
110
140
|
async onImport(path, database) {
|
|
111
141
|
await (0, process_util_1.exec)("mysql", [...(await this.buildConnectionArgs(false)), database], null, {
|
|
112
142
|
pipe: {
|
|
@@ -9,6 +9,7 @@ export declare class PostgresqlDumpTask extends SqlDumpTaskAbstract<PostgresqlDu
|
|
|
9
9
|
onCreateDatabase(database: TargetDatabaseType): Promise<void>;
|
|
10
10
|
onExecQuery(query: string): Promise<import("../util/process-util").ExecResultType>;
|
|
11
11
|
onFetchTableNames(database: string): Promise<string[]>;
|
|
12
|
-
|
|
12
|
+
onExportTables(tableNames: string[], output: string): Promise<void>;
|
|
13
|
+
onExportStoredPrograms(): Promise<void>;
|
|
13
14
|
onImport(path: string, database: string): Promise<void>;
|
|
14
15
|
}
|
|
@@ -71,7 +71,7 @@ class PostgresqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
71
71
|
table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
72
72
|
`);
|
|
73
73
|
}
|
|
74
|
-
async
|
|
74
|
+
async onExportTables(tableNames, output) {
|
|
75
75
|
const stream = (0, fs_1.createWriteStream)(output);
|
|
76
76
|
await Promise.all([
|
|
77
77
|
new Promise((resolve, reject) => {
|
|
@@ -90,6 +90,9 @@ class PostgresqlDumpTask extends SqlDumpTaskAbstract_1.SqlDumpTaskAbstract {
|
|
|
90
90
|
}),
|
|
91
91
|
]);
|
|
92
92
|
}
|
|
93
|
+
async onExportStoredPrograms() {
|
|
94
|
+
throw new Error(`Method not implemented: onExportStoredPrograms`);
|
|
95
|
+
}
|
|
93
96
|
async onImport(path, database) {
|
|
94
97
|
await (0, process_util_1.exec)("psql", [...(await this.buildConnectionArgs(database)), "-f", (0, path_1.normalize)(path)], undefined, {
|
|
95
98
|
log: this.verbose,
|
|
@@ -14,6 +14,7 @@ export declare type SqlDumpTaskConfigType = {
|
|
|
14
14
|
port?: number;
|
|
15
15
|
database: string;
|
|
16
16
|
username: string;
|
|
17
|
+
storedPrograms?: boolean;
|
|
17
18
|
targetDatabase?: TargetDatabaseType;
|
|
18
19
|
includeTables?: string[];
|
|
19
20
|
excludeTables?: string[];
|
|
@@ -28,7 +29,8 @@ export declare abstract class SqlDumpTaskAbstract<TConfig extends SqlDumpTaskCon
|
|
|
28
29
|
abstract onDatabaseIsEmpty(databaseName: string): Promise<boolean>;
|
|
29
30
|
abstract onFetchTableNames(database: string): Promise<string[]>;
|
|
30
31
|
abstract onExecQuery(query: string): ReturnType<typeof exec>;
|
|
31
|
-
abstract
|
|
32
|
+
abstract onExportTables(tableNames: string[], output: string): Promise<void>;
|
|
33
|
+
abstract onExportStoredPrograms(output: string): Promise<void>;
|
|
32
34
|
abstract onImport(path: string, database: string): Promise<void>;
|
|
33
35
|
onBackup(data: BackupDataType): Promise<void>;
|
|
34
36
|
onRestore(data: RestoreDataType): Promise<void>;
|
|
@@ -44,6 +44,7 @@ exports.sqlDumpTaskDefinition = {
|
|
|
44
44
|
collate: { type: "string" },
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
+
storedPrograms: { type: "boolean" },
|
|
47
48
|
includeTables: (0, DefinitionEnum_1.makeRef)(DefinitionEnum_1.DefinitionEnum.stringListUtil),
|
|
48
49
|
excludeTables: (0, DefinitionEnum_1.makeRef)(DefinitionEnum_1.DefinitionEnum.stringListUtil),
|
|
49
50
|
oneFileByTable: { type: "boolean" },
|
|
@@ -116,7 +117,7 @@ class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
|
116
117
|
await (0, promises_1.mkdir)(outputPath, { recursive: true });
|
|
117
118
|
if (!this.config.oneFileByTable) {
|
|
118
119
|
const outPath = (0, path_1.join)(outputPath, serializeSqlFile({ database: this.config.database }));
|
|
119
|
-
await this.
|
|
120
|
+
await this.onExportTables(tableNames, outPath);
|
|
120
121
|
}
|
|
121
122
|
else {
|
|
122
123
|
let current = 0;
|
|
@@ -129,9 +130,13 @@ class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
|
129
130
|
});
|
|
130
131
|
current++;
|
|
131
132
|
const outPath = (0, path_1.join)(outputPath, serializeSqlFile({ table: tableName }));
|
|
132
|
-
await this.
|
|
133
|
+
await this.onExportTables([tableName], outPath);
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
if (this.config.storedPrograms) {
|
|
137
|
+
const outPath = (0, path_1.join)(outputPath, "stored-programs.sql");
|
|
138
|
+
await this.onExportStoredPrograms(outPath);
|
|
139
|
+
}
|
|
135
140
|
}
|
|
136
141
|
async onRestore(data) {
|
|
137
142
|
const restorePath = data.package.restorePath;
|
|
@@ -155,7 +160,7 @@ class SqlDumpTaskAbstract extends TaskAbstract_1.TaskAbstract {
|
|
|
155
160
|
database: database.name,
|
|
156
161
|
});
|
|
157
162
|
}
|
|
158
|
-
const items = (await (0,
|
|
163
|
+
const items = (await (0, fs_util_1.readDir)(restorePath))
|
|
159
164
|
.map(parseSqlFile)
|
|
160
165
|
.filter((v) => !!v);
|
|
161
166
|
// Database check
|
package/cli.js
CHANGED
|
@@ -104,7 +104,7 @@ function parseArgs(args) {
|
|
|
104
104
|
(0, process_util_1.onExit)((eventName, error) => {
|
|
105
105
|
if (eventName !== "exit") {
|
|
106
106
|
process.stdout.write(cli_util_1.showCursorCommand);
|
|
107
|
-
console.
|
|
107
|
+
console.info(`\nClosing... (reason: ${eventName})`);
|
|
108
108
|
if (error instanceof Error)
|
|
109
109
|
console.error((0, chalk_1.red)(error.stack));
|
|
110
110
|
}
|
package/config.schema.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"name": {
|
|
21
21
|
"type": "string"
|
|
22
22
|
},
|
|
23
|
+
"mirrorRepoNames": {
|
|
24
|
+
"$ref": "#/definitions/stringlist-util"
|
|
25
|
+
},
|
|
23
26
|
"enabled": {
|
|
24
27
|
"anyOf": [
|
|
25
28
|
{
|
|
@@ -713,6 +716,9 @@
|
|
|
713
716
|
}
|
|
714
717
|
}
|
|
715
718
|
},
|
|
719
|
+
"storedPrograms": {
|
|
720
|
+
"type": "boolean"
|
|
721
|
+
},
|
|
716
722
|
"includeTables": {
|
|
717
723
|
"$ref": "#/definitions/stringlist-util"
|
|
718
724
|
},
|
package/package.json
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datatruck/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"ajv": "^8.11.0",
|
|
6
6
|
"async": "^3.2.4",
|
|
7
7
|
"chalk": "^4.1.2",
|
|
8
8
|
"cli-table3": "^0.6.2",
|
|
9
|
-
"commander": "^9.
|
|
10
|
-
"dayjs": "^1.11.
|
|
9
|
+
"commander": "^9.4.0",
|
|
10
|
+
"dayjs": "^1.11.5",
|
|
11
11
|
"fast-glob": "^3.2.11",
|
|
12
12
|
"micromatch": "^4.0.5",
|
|
13
|
-
"sqlite": "^4.1.
|
|
14
|
-
"sqlite3": "^5.0.
|
|
13
|
+
"sqlite": "^4.1.2",
|
|
14
|
+
"sqlite3": "^5.0.11"
|
|
15
15
|
},
|
|
16
16
|
"optionalDependencies": {
|
|
17
|
-
"ts-node": "^10.
|
|
18
|
-
"yaml": "^2.1.
|
|
17
|
+
"ts-node": "^10.9.1",
|
|
18
|
+
"yaml": "^2.1.1"
|
|
19
19
|
},
|
|
20
20
|
"engine": {
|
|
21
21
|
"node": ">=16.0.0"
|
|
22
22
|
},
|
|
23
|
-
"bin": {
|
|
24
|
-
"datatruck": "bin.js",
|
|
25
|
-
"dtt": "bin.js"
|
|
26
|
-
},
|
|
27
23
|
"description": "Tool for creating and managing backups",
|
|
28
24
|
"homepage": "https://github.com/swordev/datatruck#readme",
|
|
29
25
|
"bugs": {
|
|
@@ -38,5 +34,9 @@
|
|
|
38
34
|
"name": "Juanra GM",
|
|
39
35
|
"email": "juanrgm724@gmail.com",
|
|
40
36
|
"url": "https://github.com/juanrgm"
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"datatruck": "bin.js",
|
|
40
|
+
"dtt": "bin.js"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/util/GitUtil.js
CHANGED
|
@@ -3,7 +3,6 @@ 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 promises_1 = require("fs/promises");
|
|
7
6
|
class GitUtil {
|
|
8
7
|
constructor(options) {
|
|
9
8
|
this.options = options;
|
|
@@ -16,7 +15,7 @@ class GitUtil {
|
|
|
16
15
|
}
|
|
17
16
|
async canBeInit(repo) {
|
|
18
17
|
return ((0, fs_util_1.isLocalDir)(repo) &&
|
|
19
|
-
(!(await (0, fs_util_1.checkDir)(repo)) || !(await (0,
|
|
18
|
+
(!(await (0, fs_util_1.checkDir)(repo)) || !(await (0, fs_util_1.readDir)(repo)).length));
|
|
20
19
|
}
|
|
21
20
|
async clone(options) {
|
|
22
21
|
return await this.exec([
|
package/util/ResticUtil.d.ts
CHANGED
|
@@ -86,6 +86,10 @@ export declare class ResticUtil {
|
|
|
86
86
|
allowEmptySnapshot?: boolean;
|
|
87
87
|
onStream?: (data: BackupStreamType) => void;
|
|
88
88
|
}): Promise<ExecResultType>;
|
|
89
|
+
copy(options: {
|
|
90
|
+
id: string;
|
|
91
|
+
onStream?: (data: BackupStreamType) => Promise<void>;
|
|
92
|
+
}): Promise<ExecResultType>;
|
|
89
93
|
restore(options: {
|
|
90
94
|
id: string;
|
|
91
95
|
target: string;
|
package/util/ResticUtil.js
CHANGED
|
@@ -154,6 +154,22 @@ class ResticUtil {
|
|
|
154
154
|
throw error;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
async copy(options) {
|
|
158
|
+
return await this.exec(["copy", "--json", options.id], {
|
|
159
|
+
stderr: {
|
|
160
|
+
toExitCode: true,
|
|
161
|
+
},
|
|
162
|
+
stdout: {
|
|
163
|
+
...(options.onStream && {
|
|
164
|
+
onData: async (data) => {
|
|
165
|
+
if (data.startsWith("{") && data.endsWith("}")) {
|
|
166
|
+
await options.onStream?.(JSON.parse(data));
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
157
173
|
async restore(options) {
|
|
158
174
|
return await this.exec(["restore", "--json", options.id, "--target", options.target], {
|
|
159
175
|
stderr: {
|
package/util/cli-util.js
CHANGED
|
@@ -32,11 +32,11 @@ function logVars(data) {
|
|
|
32
32
|
let first = true;
|
|
33
33
|
for (const key in data) {
|
|
34
34
|
if (first) {
|
|
35
|
-
console.
|
|
35
|
+
console.info();
|
|
36
36
|
first = false;
|
|
37
37
|
}
|
|
38
38
|
const value = data[key];
|
|
39
|
-
console.
|
|
39
|
+
console.info(`${chalk_1.default.cyan(key)}${chalk_1.default.grey(":")} ${chalk_1.default.white(value ?? "")}`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
exports.logVars = logVars;
|
package/util/date-util.d.ts
CHANGED
|
@@ -12,5 +12,5 @@ export declare function filterByLast<TItem extends {
|
|
|
12
12
|
}>(items: TItem[], options: FilterByLastOptionsType, reasons?: Record<number, string[]>): TItem[];
|
|
13
13
|
export declare function createChron(): {
|
|
14
14
|
start: () => number;
|
|
15
|
-
elapsed: (formatted?: boolean
|
|
15
|
+
elapsed: (formatted?: boolean) => string | number;
|
|
16
16
|
};
|
package/util/entity-util.d.ts
CHANGED
package/util/fs-util.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
2
3
|
import { Interface } from "readline";
|
|
3
4
|
export declare function isLocalDir(path: string): boolean;
|
|
4
5
|
export declare function isDirEmpty(path: string): Promise<boolean>;
|
|
@@ -23,6 +24,7 @@ export declare function mkTmpDir(prefix: string, id?: string): Promise<string>;
|
|
|
23
24
|
export declare function readPartialFile(path: string, positions: [number, number?]): Promise<string>;
|
|
24
25
|
export declare function checkFile(path: string): Promise<boolean>;
|
|
25
26
|
export declare function checkDir(path: string): Promise<boolean>;
|
|
27
|
+
export declare function readDir(path: string): Promise<string[]>;
|
|
26
28
|
export declare function forEachFile(dirPath: string, cb: (path: string, dir: boolean) => void, includeDir?: boolean): Promise<void>;
|
|
27
29
|
export declare function writeGitIgnoreList(options: {
|
|
28
30
|
paths: NodeJS.ReadableStream | string[];
|
package/util/fs-util.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.cpy = exports.writePathLists = exports.writeGitIgnoreList = exports.forEachFile = exports.checkDir = exports.checkFile = exports.readPartialFile = exports.mkTmpDir = exports.tmpDir = exports.sessionTmpDir = exports.parentTmpDir = exports.existsFile = exports.findFile = exports.parsePackageFile = exports.parseFile = exports.parseFileExtensions = exports.readdirIfExists = exports.writeJSONFile = exports.existsDir = exports.ensureEmptyDir = exports.mkdirIfNotExists = exports.isDirEmpty = exports.isLocalDir = void 0;
|
|
6
|
+
exports.cpy = exports.writePathLists = exports.writeGitIgnoreList = exports.forEachFile = exports.readDir = exports.checkDir = exports.checkFile = exports.readPartialFile = exports.mkTmpDir = exports.tmpDir = exports.sessionTmpDir = exports.parentTmpDir = exports.existsFile = exports.findFile = exports.parsePackageFile = exports.parseFile = exports.parseFileExtensions = exports.readdirIfExists = exports.writeJSONFile = exports.existsDir = exports.ensureEmptyDir = exports.mkdirIfNotExists = exports.isDirEmpty = exports.isLocalDir = void 0;
|
|
7
7
|
const globalData_1 = __importDefault(require("../globalData"));
|
|
8
8
|
const path_util_1 = require("./path-util");
|
|
9
9
|
const async_1 = require("async");
|
|
@@ -21,7 +21,7 @@ function isLocalDir(path) {
|
|
|
21
21
|
}
|
|
22
22
|
exports.isLocalDir = isLocalDir;
|
|
23
23
|
async function isDirEmpty(path) {
|
|
24
|
-
const files = await (
|
|
24
|
+
const files = await readDir(path);
|
|
25
25
|
return !files.length;
|
|
26
26
|
}
|
|
27
27
|
exports.isDirEmpty = isDirEmpty;
|
|
@@ -57,7 +57,7 @@ exports.writeJSONFile = writeJSONFile;
|
|
|
57
57
|
async function readdirIfExists(path) {
|
|
58
58
|
if (!(await existsDir(path)))
|
|
59
59
|
return [];
|
|
60
|
-
return await (
|
|
60
|
+
return await readDir(path);
|
|
61
61
|
}
|
|
62
62
|
exports.readdirIfExists = readdirIfExists;
|
|
63
63
|
exports.parseFileExtensions = ["json", "js", "ts", "yaml", "yml"];
|
|
@@ -179,8 +179,25 @@ async function checkDir(path) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
exports.checkDir = checkDir;
|
|
182
|
+
async function readDir(path) {
|
|
183
|
+
try {
|
|
184
|
+
return await (0, promises_1.readdir)(path);
|
|
185
|
+
}
|
|
186
|
+
catch (anyError) {
|
|
187
|
+
const nodeError = anyError;
|
|
188
|
+
if (nodeError.code === "ENOENT") {
|
|
189
|
+
const error = new Error(nodeError.message);
|
|
190
|
+
error.code = nodeError.code;
|
|
191
|
+
error.errno = nodeError.errno;
|
|
192
|
+
error.path = nodeError.path;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
throw anyError;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.readDir = readDir;
|
|
182
199
|
async function forEachFile(dirPath, cb, includeDir) {
|
|
183
|
-
const files = await (
|
|
200
|
+
const files = await readDir(dirPath);
|
|
184
201
|
for (const file of files) {
|
|
185
202
|
const filePath = (0, path_1.join)(dirPath, file);
|
|
186
203
|
if ((await (0, promises_1.stat)(filePath)).isDirectory()) {
|
package/util/process-util.d.ts
CHANGED
package/CHANGELOG.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# @datatruck/cli
|
|
2
|
-
|
|
3
|
-
## 0.7.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- [`3b8d6da`](https://github.com/swordev/datatruck/commit/3b8d6da01495799aceb848a63b35b8c46a7d1b0e) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `--package-task` cli option
|
|
8
|
-
|
|
9
|
-
* [`69b34a0`](https://github.com/swordev/datatruck/commit/69b34a02b9cade48df2b071a92a8f79d5cfec23e) Thanks [@juanrgm](https://github.com/juanrgm)! - Allow restore multiple backups over the same database
|
|
10
|
-
|
|
11
|
-
- [`69caf26`](https://github.com/swordev/datatruck/commit/69caf26881272331bd4c8d7d345b3b85d33e33ac) Thanks [@juanrgm](https://github.com/juanrgm)! - Add cli short option to `--tag`
|
|
12
|
-
|
|
13
|
-
* [`377f0de`](https://github.com/swordev/datatruck/commit/377f0de345c9c8f45c772ac47e4ded81e91725d7) Thanks [@juanrgm](https://github.com/juanrgm)! - Rename cli short option to `-rt`
|
|
14
|
-
|
|
15
|
-
### Patch Changes
|
|
16
|
-
|
|
17
|
-
- [`c03200a`](https://github.com/swordev/datatruck/commit/c03200a6347d1e9f9fdad86dcb22df30bbefcab4) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix `sql-dump` tasks
|
|
18
|
-
|
|
19
|
-
* [`f56a4bc`](https://github.com/swordev/datatruck/commit/f56a4bcb429a674c13f32de73985cd67eb1acc23) Thanks [@juanrgm](https://github.com/juanrgm)! - Show full error message
|
|
20
|
-
|
|
21
|
-
- [`4324422`](https://github.com/swordev/datatruck/commit/4324422550474619811a8d455af55bc6e3b08aeb) Thanks [@juanrgm](https://github.com/juanrgm)! - Use connection port in `mysql-dump` task
|
|
22
|
-
|
|
23
|
-
## 0.6.1
|
|
24
|
-
|
|
25
|
-
### Patch Changes
|
|
26
|
-
|
|
27
|
-
- [`0ba6229`](https://github.com/swordev/datatruck/commit/0ba6229348c109a59783e72242ab7c0e61f25e36) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix progress bar in restic repository
|
|
28
|
-
|
|
29
|
-
## 0.6.0
|
|
30
|
-
|
|
31
|
-
### Minor Changes
|
|
32
|
-
|
|
33
|
-
- [`0c6877d`](https://github.com/swordev/datatruck/commit/0c6877d189761e75dd434b0a8d72b71621d024de) Thanks [@juanrgm](https://github.com/juanrgm)! - Show more progress stats
|
|
34
|
-
|
|
35
|
-
* [`751e1f6`](https://github.com/swordev/datatruck/commit/751e1f6d6b33d3fa96eb40d998fdd140ce0e3875) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `fileCopyConcurrency` option
|
|
36
|
-
|
|
37
|
-
- [`05487e6`](https://github.com/swordev/datatruck/commit/05487e6a33f875a3afb7ff0815b16da6f2a41301) Thanks [@juanrgm](https://github.com/juanrgm)! - Parse InnoDB error in `MariadbTask` to avoid infinite wait
|
|
38
|
-
|
|
39
|
-
### Patch Changes
|
|
40
|
-
|
|
41
|
-
- [`b62a6f8`](https://github.com/swordev/datatruck/commit/b62a6f8a82409339afd65d4f96476eb57bbfb5a2) Thanks [@juanrgm](https://github.com/juanrgm)! - Resolve target/restore path in local repository
|
|
42
|
-
|
|
43
|
-
## 0.5.0
|
|
44
|
-
|
|
45
|
-
### Minor Changes
|
|
46
|
-
|
|
47
|
-
- [`5aeb2af`](https://github.com/swordev/datatruck/commit/5aeb2afb96692e00bdba501b58df9cc0e02dceaa) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `enabled` option to repository config
|
|
48
|
-
|
|
49
|
-
* [`75de836`](https://github.com/swordev/datatruck/commit/75de8369356cf02ed3fd5c58b1f9bea66432cda8) Thanks [@juanrgm](https://github.com/juanrgm)! - Allow restic password without file
|
|
50
|
-
|
|
51
|
-
## 0.4.0
|
|
52
|
-
|
|
53
|
-
### Minor Changes
|
|
54
|
-
|
|
55
|
-
- [`eeb00a6`](https://github.com/swordev/datatruck/commit/eeb00a69d75c91da40711ae79475612b1d5193b6) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `tempDir` config option
|
|
56
|
-
|
|
57
|
-
## 0.3.2
|
|
58
|
-
|
|
59
|
-
### Patch Changes
|
|
60
|
-
|
|
61
|
-
- [`8957c3b`](https://github.com/swordev/datatruck/commit/8957c3b5846606db8b825fef357445210f2a3ac3) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix restic progress parser
|
|
62
|
-
|
|
63
|
-
* [`2989718`](https://github.com/swordev/datatruck/commit/29897185e3d6659359d51ab2212351005137f86c) Thanks [@juanrgm](https://github.com/juanrgm)! - Show closing reason
|
|
64
|
-
|
|
65
|
-
- [`b9e0843`](https://github.com/swordev/datatruck/commit/b9e0843c7970944cfd30a7d2a543f515adfa60e4) Thanks [@juanrgm](https://github.com/juanrgm)! - Show restic progress in megabytes
|
|
66
|
-
|
|
67
|
-
## 0.3.1
|
|
68
|
-
|
|
69
|
-
### Patch Changes
|
|
70
|
-
|
|
71
|
-
- [`c3bb4c6`](https://github.com/swordev/datatruck/commit/c3bb4c609887c5525cf35487ea237750addb6e75) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix restic stdout parser
|
|
72
|
-
|
|
73
|
-
## 0.3.0
|
|
74
|
-
|
|
75
|
-
### Minor Changes
|
|
76
|
-
|
|
77
|
-
- [`d63fd25`](https://github.com/swordev/datatruck/commit/d63fd25ffa8d2e539d2125dfd6a3f55020086804) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `snapshotDate` param
|
|
78
|
-
|
|
79
|
-
* [`486ef4a`](https://github.com/swordev/datatruck/commit/486ef4add27ae1dbfd166b16c257522f43537ecd) Thanks [@juanrgm](https://github.com/juanrgm)! - Resolve params in `include` and `exclude`
|
|
80
|
-
|
|
81
|
-
- [`617dae2`](https://github.com/swordev/datatruck/commit/617dae2c8ed90e6e65e8109f03cfad0e64bd7c02) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `script` task
|
|
82
|
-
|
|
83
|
-
### Patch Changes
|
|
84
|
-
|
|
85
|
-
- [`d1b3ea9`](https://github.com/swordev/datatruck/commit/d1b3ea9c9540d30898c00490963523a4fbc68193) Thanks [@juanrgm](https://github.com/juanrgm)! - Avoid use gitignore if is not necessary in restic repository
|
|
86
|
-
|
|
87
|
-
## 0.2.0
|
|
88
|
-
|
|
89
|
-
### Minor Changes
|
|
90
|
-
|
|
91
|
-
- [`120460c`](https://github.com/swordev/datatruck/commit/120460c8824cef4184e43f571a4cc0798b899b66) Thanks [@juanrgm](https://github.com/juanrgm)! - Enable `include` option in restic repository
|
|
92
|
-
|
|
93
|
-
### Patch Changes
|
|
94
|
-
|
|
95
|
-
- [`e30ede3`](https://github.com/swordev/datatruck/commit/e30ede371bc7ab3fc1cd47758fdac7a28e8e2705) Thanks [@juanrgm](https://github.com/juanrgm)! - Resolve `RESTIC_PASSWORD_FILE` path
|
|
96
|
-
|
|
97
|
-
* [`8539d28`](https://github.com/swordev/datatruck/commit/8539d285b2c51d700aa811cd772d573fa0d613eb) Thanks [@juanrgm](https://github.com/juanrgm)! - Allow empty backup in restic repository
|
|
98
|
-
|
|
99
|
-
## 0.1.0
|
|
100
|
-
|
|
101
|
-
### Minor Changes
|
|
102
|
-
|
|
103
|
-
- [`88d46cd`](https://github.com/swordev/datatruck/commit/88d46cd56293df4c6fc21a9ad61d6236ac91f325) Thanks [@juanrgm](https://github.com/juanrgm)! - Add `custom` output format
|
|
104
|
-
|
|
105
|
-
### Patch Changes
|
|
106
|
-
|
|
107
|
-
- [`24a1e5e`](https://github.com/swordev/datatruck/commit/24a1e5e86336e7a92556287e49548dc542f0e579) Thanks [@juanrgm](https://github.com/juanrgm)! - Update dependencies
|
|
108
|
-
|
|
109
|
-
## 0.0.6
|
|
110
|
-
|
|
111
|
-
### Patch Changes
|
|
112
|
-
|
|
113
|
-
- [`8de6e6c`](https://github.com/swordev/datatruck/commit/8de6e6ceddb59635cb4634d884e7690eeaf59bac) Thanks [@juanrgm](https://github.com/juanrgm)! - Publish migrations
|
|
114
|
-
|
|
115
|
-
## 0.0.5
|
|
116
|
-
|
|
117
|
-
### Patch Changes
|
|
118
|
-
|
|
119
|
-
- [`78cb0c1`](https://github.com/swordev/datatruck/commit/78cb0c17558543841cd7080dc4c672e6cbfd5634) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix docker image
|
|
120
|
-
|
|
121
|
-
## 0.0.4
|
|
122
|
-
|
|
123
|
-
### Patch Changes
|
|
124
|
-
|
|
125
|
-
- [`d9e534b`](https://github.com/swordev/datatruck/commit/d9e534bd968acf9cd1c93f20e6152c004cb1f23b) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix package file read
|
|
126
|
-
|
|
127
|
-
* [`b882c58`](https://github.com/swordev/datatruck/commit/b882c58183e9a75abc876645e18d7b67186dd662) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix read of migrations
|
|
128
|
-
|
|
129
|
-
## 0.0.3
|
|
130
|
-
|
|
131
|
-
### Patch Changes
|
|
132
|
-
|
|
133
|
-
- [`051a7da`](https://github.com/swordev/datatruck/commit/051a7da225fcfea1c30a4fbfa8aea1b8f5538367) Thanks [@juanrgm](https://github.com/juanrgm)! - Fix dist files
|
|
134
|
-
|
|
135
|
-
## 0.0.2
|
|
136
|
-
|
|
137
|
-
### Patch Changes
|
|
138
|
-
|
|
139
|
-
- [`0911351`](https://github.com/swordev/datatruck/commit/09113517e1a77f2d2a1e19e4c3d9af7da1e28415) Thanks [@juanrgm](https://github.com/juanrgm)! - Publish docker image
|