@datatruck/restic 0.0.4 → 0.0.6
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/lib/actions/backup.d.ts +2 -7
- package/lib/actions/backup.js +37 -30
- package/lib/actions/copy.d.ts +0 -1
- package/lib/actions/copy.js +11 -13
- package/lib/actions/init.js +1 -1
- package/lib/actions/prune.js +10 -11
- package/lib/actions/run.js +9 -4
- package/lib/bin.d.ts +1 -0
- package/lib/bin.js +1 -0
- package/lib/create-bin.js +16 -11
- package/lib/utils/fs.d.ts +1 -0
- package/lib/utils/fs.js +1 -0
- package/lib/utils/mysql.js +20 -3
- package/lib/utils/ntfy.d.ts +4 -1
- package/lib/utils/ntfy.js +15 -9
- package/lib/utils/string.d.ts +1 -0
- package/lib/utils/string.js +10 -0
- package/lib/utils/tags.d.ts +3 -0
- package/lib/utils/tags.js +37 -0
- package/package.json +2 -2
package/lib/actions/backup.d.ts
CHANGED
|
@@ -5,16 +5,11 @@ export type BackupOptions = {
|
|
|
5
5
|
prune?: boolean;
|
|
6
6
|
};
|
|
7
7
|
export type CommonResticBackupTags = {
|
|
8
|
+
dt: boolean;
|
|
8
9
|
id: string;
|
|
9
|
-
shortId: string;
|
|
10
|
-
hostname: string;
|
|
11
|
-
date: string;
|
|
12
|
-
vendor: string;
|
|
13
|
-
version: string;
|
|
14
10
|
};
|
|
15
11
|
export type ResticBackupTags = CommonResticBackupTags & {
|
|
16
|
-
|
|
17
|
-
tags?: string[];
|
|
12
|
+
pkg: string;
|
|
18
13
|
};
|
|
19
14
|
export declare class Backup extends Action {
|
|
20
15
|
protected runSingle(repoName: string, pkgName: string, tags: CommonResticBackupTags): Promise<{
|
package/lib/actions/backup.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { createRunner, safeRun } from "../utils/async.js";
|
|
2
2
|
import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
|
|
3
3
|
import { MySQLDump } from "../utils/mysql.js";
|
|
4
|
+
import { randomString } from "../utils/string.js";
|
|
5
|
+
import { stringifyTags } from "../utils/tags.js";
|
|
4
6
|
import { Action } from "./base.js";
|
|
5
7
|
import { Prune } from "./prune.js";
|
|
6
|
-
import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
|
|
7
|
-
import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
|
|
8
8
|
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
9
9
|
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
10
10
|
import { progressPercent } from "@datatruck/cli/utils/math.js";
|
|
11
11
|
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
12
12
|
import { match } from "@datatruck/cli/utils/string.js";
|
|
13
|
-
import { randomUUID } from "crypto";
|
|
14
|
-
import { hostname } from "os";
|
|
15
13
|
export class Backup extends Action {
|
|
16
14
|
async runSingle(repoName, pkgName, tags) {
|
|
17
15
|
const repo = this.cm.findRepository(repoName);
|
|
@@ -23,25 +21,35 @@ export class Backup extends Action {
|
|
|
23
21
|
RESTIC_REPOSITORY: repo.uri,
|
|
24
22
|
},
|
|
25
23
|
});
|
|
24
|
+
let logId;
|
|
26
25
|
let space;
|
|
27
26
|
let snapshotsAmount;
|
|
28
27
|
let bytes = 0;
|
|
29
28
|
let files = 0;
|
|
30
29
|
return await createRunner(async () => {
|
|
30
|
+
logId = await this.ntfy.send("Backup", {
|
|
31
|
+
Repository: repo.name,
|
|
32
|
+
Package: pkg.name,
|
|
33
|
+
});
|
|
31
34
|
await restic.tryInit();
|
|
32
35
|
const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
|
|
33
36
|
space = await checkDiskSpace({
|
|
34
37
|
minFreeSpace: this.config.minFreeSpace,
|
|
35
38
|
minFreeSpacePath: targetPath ?? process.cwd(),
|
|
36
39
|
targetPath,
|
|
37
|
-
rutine: () => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tags:
|
|
42
|
-
};
|
|
43
|
-
return restic.backup({
|
|
44
|
-
|
|
40
|
+
rutine: async () => {
|
|
41
|
+
const last = await restic.snapshots({
|
|
42
|
+
latest: 1,
|
|
43
|
+
host: this.config.hostname,
|
|
44
|
+
tags: stringifyTags({ pkg: pkg.name }),
|
|
45
|
+
});
|
|
46
|
+
return await restic.backup({
|
|
47
|
+
parent: last.at(0)?.id,
|
|
48
|
+
host: this.config.hostname,
|
|
49
|
+
tags: stringifyTags({
|
|
50
|
+
...tags,
|
|
51
|
+
pkg: pkg.name,
|
|
52
|
+
}),
|
|
45
53
|
paths: [pkg.path],
|
|
46
54
|
exclude: pkg.exclude,
|
|
47
55
|
onStream(data) {
|
|
@@ -55,9 +63,7 @@ export class Backup extends Action {
|
|
|
55
63
|
});
|
|
56
64
|
const snapshots = await restic.snapshots({
|
|
57
65
|
json: true,
|
|
58
|
-
tags:
|
|
59
|
-
ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
|
|
60
|
-
],
|
|
66
|
+
tags: stringifyTags({ pkg: pkg.name }),
|
|
61
67
|
});
|
|
62
68
|
snapshotsAmount = snapshots.length;
|
|
63
69
|
}).start(async (data) => {
|
|
@@ -70,7 +76,7 @@ export class Backup extends Action {
|
|
|
70
76
|
Snapshots: snapshotsAmount,
|
|
71
77
|
Duration: data.duration,
|
|
72
78
|
Error: data.error?.message,
|
|
73
|
-
}, data.error);
|
|
79
|
+
}, { error: data.error, logId });
|
|
74
80
|
return {
|
|
75
81
|
error: data.error,
|
|
76
82
|
files,
|
|
@@ -87,12 +93,17 @@ export class Backup extends Action {
|
|
|
87
93
|
const sqlDumps = [];
|
|
88
94
|
const backups = [];
|
|
89
95
|
let localRepositoryPaths = [];
|
|
96
|
+
const tags = {
|
|
97
|
+
dt: true,
|
|
98
|
+
id: randomString(8),
|
|
99
|
+
};
|
|
90
100
|
await createRunner(async () => {
|
|
91
101
|
const repositories = this.cm.filterRepositories(options.repositories);
|
|
92
102
|
const packages = this.cm.filterPackages(options.packages);
|
|
93
103
|
const packageNames = packages.map((p) => p.name);
|
|
94
104
|
const tasks = this.filterTasks(packageNames);
|
|
95
105
|
await this.ntfy.send(`Backup start`, {
|
|
106
|
+
Id: tags.id,
|
|
96
107
|
Repositories: repositories.length,
|
|
97
108
|
Packages: packageNames.length,
|
|
98
109
|
Tasks: tasks?.length,
|
|
@@ -100,16 +111,6 @@ export class Backup extends Action {
|
|
|
100
111
|
localRepositoryPaths = repositories
|
|
101
112
|
.filter((repo) => isLocalDir(repo.uri))
|
|
102
113
|
.map((repo) => repo.uri);
|
|
103
|
-
const tags = {
|
|
104
|
-
id: randomUUID().replaceAll("-", ""),
|
|
105
|
-
get shortId() {
|
|
106
|
-
return this.id.slice(0, 8);
|
|
107
|
-
},
|
|
108
|
-
hostname: this.config.hostname ?? hostname(),
|
|
109
|
-
date: new Date().toISOString(),
|
|
110
|
-
vendor: "dtt-restic",
|
|
111
|
-
version: "1",
|
|
112
|
-
};
|
|
113
114
|
for (const task of tasks ?? []) {
|
|
114
115
|
if (task.type === "mysql-dump") {
|
|
115
116
|
const mysqlDump = new MySQLDump({
|
|
@@ -144,8 +145,13 @@ export class Backup extends Action {
|
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
}).start(async (data) => {
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
try {
|
|
149
|
+
for (const sqlDump of sqlDumps)
|
|
150
|
+
await sqlDump.cleanup();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(error);
|
|
154
|
+
}
|
|
149
155
|
const summary = backups.reduce((acc, p) => {
|
|
150
156
|
if (!acc[p.pkgName])
|
|
151
157
|
acc[p.pkgName] = {
|
|
@@ -174,6 +180,7 @@ export class Backup extends Action {
|
|
|
174
180
|
if (diskStats.error)
|
|
175
181
|
console.error(diskStats.error);
|
|
176
182
|
await this.ntfy.send("Backup end", {
|
|
183
|
+
Id: tags.id,
|
|
177
184
|
Duration: data.duration,
|
|
178
185
|
Size: formatBytes(size),
|
|
179
186
|
Error: data.error?.message,
|
|
@@ -181,7 +188,7 @@ export class Backup extends Action {
|
|
|
181
188
|
!!diskStats.result?.length && { key: "Disk stats", value: "" },
|
|
182
189
|
...(diskStats.result?.map((p) => ({
|
|
183
190
|
key: p.name,
|
|
184
|
-
value: `${formatBytes(p.
|
|
191
|
+
value: `${formatBytes(p.occupied)}/${formatBytes(p.total)} (${progressPercent(p.total, p.occupied)}%)`,
|
|
185
192
|
level: 1,
|
|
186
193
|
})) || []),
|
|
187
194
|
!!sqlDumpProcesses.length && { key: "SQL Dumps", value: "" },
|
|
@@ -197,7 +204,7 @@ export class Backup extends Action {
|
|
|
197
204
|
level: 1,
|
|
198
205
|
})),
|
|
199
206
|
],
|
|
200
|
-
}, error);
|
|
207
|
+
}, { error });
|
|
201
208
|
if (options.prune) {
|
|
202
209
|
await new Prune(this.config, this.global).run({
|
|
203
210
|
packages: options.packages,
|
package/lib/actions/copy.d.ts
CHANGED
package/lib/actions/copy.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { createRunner } from "../utils/async.js";
|
|
2
2
|
import { checkDiskSpace } from "../utils/fs.js";
|
|
3
|
+
import { parseTags, stringifyTags } from "../utils/tags.js";
|
|
3
4
|
import { Action } from "./base.js";
|
|
4
5
|
import { Prune } from "./prune.js";
|
|
5
|
-
import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
|
|
6
|
-
import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
|
|
7
6
|
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
8
7
|
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
9
8
|
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
@@ -22,23 +21,16 @@ export class Copy extends Action {
|
|
|
22
21
|
latest: 1,
|
|
23
22
|
group: ["path"],
|
|
24
23
|
tags: packages
|
|
25
|
-
? packages.
|
|
24
|
+
? packages.flatMap((name) => stringifyTags({ pkg: name }))
|
|
26
25
|
: undefined,
|
|
27
26
|
}));
|
|
28
27
|
return snapshots.flatMap((s) => s.snapshots);
|
|
29
28
|
}
|
|
30
|
-
findPackageTag(inTags) {
|
|
31
|
-
const tags = inTags
|
|
32
|
-
.map((t) => ResticRepository.parseSnapshotTag(t))
|
|
33
|
-
.filter((t) => !!t);
|
|
34
|
-
return tags.find((t) => t.name === SnapshotTagEnum.PACKAGE);
|
|
35
|
-
}
|
|
36
29
|
async runSingle(options) {
|
|
37
30
|
const { snapshot } = options;
|
|
38
31
|
const targetRepo = this.cm.findRepository(options.target);
|
|
39
32
|
const sourceRepo = this.cm.findRepository(options.source);
|
|
40
|
-
const
|
|
41
|
-
const packageName = pkgTag?.value;
|
|
33
|
+
const packageName = parseTags(snapshot.tags).pkg;
|
|
42
34
|
const targetPath = isLocalDir(targetRepo.uri) ? targetRepo.uri : undefined;
|
|
43
35
|
const target = new Restic({
|
|
44
36
|
log: this.verbose,
|
|
@@ -48,8 +40,14 @@ export class Copy extends Action {
|
|
|
48
40
|
["GODEBUG"]: "http2client=0",
|
|
49
41
|
},
|
|
50
42
|
});
|
|
43
|
+
let logId;
|
|
51
44
|
let space;
|
|
52
45
|
await createRunner(async () => {
|
|
46
|
+
logId = await this.ntfy.send("Copy", {
|
|
47
|
+
Id: snapshot.short_id,
|
|
48
|
+
Source: sourceRepo.name,
|
|
49
|
+
Target: targetRepo.name,
|
|
50
|
+
});
|
|
53
51
|
if (!this.initializedRepos.has(targetRepo.name)) {
|
|
54
52
|
await target.tryInit();
|
|
55
53
|
this.initializedRepos.add(targetRepo.name);
|
|
@@ -74,7 +72,7 @@ export class Copy extends Action {
|
|
|
74
72
|
}),
|
|
75
73
|
Duration: data.duration,
|
|
76
74
|
Error: data.error?.message,
|
|
77
|
-
}, data.error);
|
|
75
|
+
}, { error: data.error, logId });
|
|
78
76
|
});
|
|
79
77
|
return space?.diff ?? 0;
|
|
80
78
|
}
|
|
@@ -107,7 +105,7 @@ export class Copy extends Action {
|
|
|
107
105
|
}),
|
|
108
106
|
Duration: data.duration,
|
|
109
107
|
Error: data.error?.message,
|
|
110
|
-
}, data.error);
|
|
108
|
+
}, { error: data.error });
|
|
111
109
|
if (options.prune) {
|
|
112
110
|
await new Prune(this.config, this.global).run({
|
|
113
111
|
packages: options.packages,
|
package/lib/actions/init.js
CHANGED
package/lib/actions/prune.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { createRunner, safeRun } from "../utils/async.js";
|
|
2
2
|
import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
|
|
3
|
+
import { parseTags, stringifyTags } from "../utils/tags.js";
|
|
3
4
|
import { Action } from "./base.js";
|
|
4
|
-
import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
|
|
5
|
-
import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
|
|
6
5
|
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
7
6
|
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
8
7
|
import { progressPercent } from "@datatruck/cli/utils/math.js";
|
|
9
8
|
import { match } from "@datatruck/cli/utils/string.js";
|
|
10
9
|
export class Prune extends Action {
|
|
11
10
|
async runSingle(repoName, pkgName, policy) {
|
|
11
|
+
let logId;
|
|
12
12
|
let stats;
|
|
13
13
|
let space;
|
|
14
14
|
let removed;
|
|
15
15
|
await createRunner(async () => {
|
|
16
|
+
logId = await this.ntfy.send("Prune", {
|
|
17
|
+
Repository: repoName,
|
|
18
|
+
Package: pkgName,
|
|
19
|
+
});
|
|
16
20
|
const [restic, repo] = this.cm.createRestic(repoName, this.verbose);
|
|
17
21
|
const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
|
|
18
22
|
space = await checkDiskSpace({
|
|
@@ -24,9 +28,7 @@ export class Prune extends Action {
|
|
|
24
28
|
json: true,
|
|
25
29
|
prune: true,
|
|
26
30
|
args: ["--group-by", ""],
|
|
27
|
-
tag:
|
|
28
|
-
ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkgName),
|
|
29
|
-
],
|
|
31
|
+
tag: stringifyTags({ pkg: pkgName }),
|
|
30
32
|
});
|
|
31
33
|
stats = {
|
|
32
34
|
keep: results.at(0)?.keep?.length ?? 0,
|
|
@@ -49,7 +51,7 @@ export class Prune extends Action {
|
|
|
49
51
|
}),
|
|
50
52
|
Duration: data.duration,
|
|
51
53
|
Error: data.error?.message,
|
|
52
|
-
}, data.error);
|
|
54
|
+
}, { error: data.error, logId });
|
|
53
55
|
});
|
|
54
56
|
return {
|
|
55
57
|
diffSize: space?.diff ?? 0,
|
|
@@ -61,10 +63,7 @@ export class Prune extends Action {
|
|
|
61
63
|
if (!(await restic.checkRepository()))
|
|
62
64
|
return [];
|
|
63
65
|
const snapshots = await restic.snapshots({ json: true });
|
|
64
|
-
const packages = new Set(snapshots.map((s) =>
|
|
65
|
-
const tags = ResticRepository.parseSnapshotTags(s.tags ?? []);
|
|
66
|
-
return tags[SnapshotTagEnum.PACKAGE];
|
|
67
|
-
}));
|
|
66
|
+
const packages = new Set(snapshots.map((s) => parseTags(s.tags ?? []).pkg));
|
|
68
67
|
return [...packages].filter((v) => typeof v === "string");
|
|
69
68
|
}
|
|
70
69
|
async run(options) {
|
|
@@ -113,7 +112,7 @@ export class Prune extends Action {
|
|
|
113
112
|
!!diskStats.result?.length && { key: "Disk stats", value: "" },
|
|
114
113
|
...(diskStats.result?.map((p) => ({
|
|
115
114
|
key: p.name,
|
|
116
|
-
value: `${formatBytes(p.
|
|
115
|
+
value: `${formatBytes(p.occupied)}/${formatBytes(p.total)} (${progressPercent(p.total, p.occupied)}%)`,
|
|
117
116
|
level: 1,
|
|
118
117
|
})) || []),
|
|
119
118
|
],
|
package/lib/actions/run.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Action } from "./base.js";
|
|
2
|
+
import { logExec } from "@datatruck/cli/utils/cli.js";
|
|
2
3
|
import { spawnSync } from "child_process";
|
|
3
4
|
export class Run extends Action {
|
|
4
5
|
async run(options) {
|
|
5
6
|
const [restic] = this.cm.createRestic(options.repository, this.verbose);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
if (this.verbose)
|
|
8
|
+
logExec("restic", options.args, restic.options.env);
|
|
9
|
+
const p = spawnSync("restic", options.args, {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
env: { ...process.env, ...restic.options.env },
|
|
12
|
+
});
|
|
13
|
+
if (p.status)
|
|
14
|
+
process.exit(p.status);
|
|
10
15
|
}
|
|
11
16
|
}
|
package/lib/bin.d.ts
CHANGED
package/lib/bin.js
CHANGED
package/lib/create-bin.js
CHANGED
|
@@ -34,14 +34,17 @@ export function createBin(inConfig) {
|
|
|
34
34
|
await create.run(options);
|
|
35
35
|
});
|
|
36
36
|
program
|
|
37
|
-
.command("run"
|
|
37
|
+
.command("run")
|
|
38
38
|
.description("Run arbitrary restic command")
|
|
39
|
-
.
|
|
39
|
+
.option("-r, --repository <name>", "Repository name")
|
|
40
40
|
.argument("[args...]", "Restic arguments")
|
|
41
41
|
.allowUnknownOption()
|
|
42
42
|
.allowExcessArguments()
|
|
43
|
-
.action(async (
|
|
43
|
+
.action(async (args, options) => {
|
|
44
44
|
const { config, globalOptions } = await load();
|
|
45
|
+
if (!options.repository && config.repositories.length !== 1)
|
|
46
|
+
throw new Error("Repository name is required");
|
|
47
|
+
const repository = options.repository ?? config.repositories[0].name;
|
|
45
48
|
const run = new Run(config, globalOptions);
|
|
46
49
|
await run.run({ repository, args });
|
|
47
50
|
});
|
|
@@ -96,15 +99,17 @@ export function createBin(inConfig) {
|
|
|
96
99
|
program.parse = function (args, options) {
|
|
97
100
|
if (!args)
|
|
98
101
|
args = process.argv;
|
|
99
|
-
const [node, script, ...rest] = args;
|
|
100
|
-
const commandIndex = rest.findIndex((v) => !v.startsWith("-"));
|
|
101
|
-
const command = rest[commandIndex];
|
|
102
|
+
const [node, script, command, ...rest] = args;
|
|
102
103
|
if (command === "run") {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const repositoryIndex = rest.findIndex((v) => v === "-r" || v === "--repository");
|
|
105
|
+
if (repositoryIndex !== -1) {
|
|
106
|
+
const repositoryName = rest[repositoryIndex + 1];
|
|
107
|
+
const others = rest.filter((_, i) => i !== repositoryIndex && i !== repositoryIndex + 1);
|
|
108
|
+
args = [node, script, command, "-r", repositoryName, "--", ...others];
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
args = [node, script, command, "--", ...rest];
|
|
112
|
+
}
|
|
108
113
|
}
|
|
109
114
|
return parse(args, options);
|
|
110
115
|
};
|
package/lib/utils/fs.d.ts
CHANGED
package/lib/utils/fs.js
CHANGED
package/lib/utils/mysql.js
CHANGED
|
@@ -54,6 +54,7 @@ import { runParallel } from "@datatruck/cli/utils/async.js";
|
|
|
54
54
|
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
55
55
|
import { duration } from "@datatruck/cli/utils/date.js";
|
|
56
56
|
import { ensureFreeDiskSpace, fetchDiskStats, } from "@datatruck/cli/utils/fs.js";
|
|
57
|
+
import { progressPercent } from "@datatruck/cli/utils/math.js";
|
|
57
58
|
import { createMysqlCli } from "@datatruck/cli/utils/mysql.js";
|
|
58
59
|
import { match } from "@datatruck/cli/utils/string.js";
|
|
59
60
|
import { existsSync } from "fs";
|
|
@@ -101,7 +102,7 @@ export class MySQLDump {
|
|
|
101
102
|
for (const outPath of this.outPaths) {
|
|
102
103
|
const parentFolder = basename(dirname(outPath));
|
|
103
104
|
if (!parentFolder.startsWith("sql-dump"))
|
|
104
|
-
throw new Error(`sql-dump out dir must begins with 'sql-dump': ${outPath}`);
|
|
105
|
+
throw new Error(`sql-dump out dir base name must begins with 'sql-dump': ${outPath}`);
|
|
105
106
|
if (existsSync(outPath))
|
|
106
107
|
await rm(outPath, { recursive: true });
|
|
107
108
|
if (!init)
|
|
@@ -129,7 +130,20 @@ export class MySQLDump {
|
|
|
129
130
|
for (const item of items)
|
|
130
131
|
this.outPaths.add(dirname(item.out));
|
|
131
132
|
await this.cleanup(true);
|
|
133
|
+
let logId;
|
|
134
|
+
let logIntervalId;
|
|
132
135
|
try {
|
|
136
|
+
logId = await this.ntfy.send("SQL dump", {
|
|
137
|
+
Name: item.name,
|
|
138
|
+
});
|
|
139
|
+
logIntervalId = setInterval(async () => {
|
|
140
|
+
await this.ntfy.send("SQL dump", {
|
|
141
|
+
Name: item.name,
|
|
142
|
+
Size: formatBytes(stats.bytes),
|
|
143
|
+
Files: `${stats.files}/${items.length} (${progressPercent(items.length, stats.files)}%)`,
|
|
144
|
+
Duration: duration(Date.now() - now),
|
|
145
|
+
}, { logId });
|
|
146
|
+
}, 30_000);
|
|
133
147
|
await runParallel({
|
|
134
148
|
items,
|
|
135
149
|
concurrency: this.options.concurrency ?? 1,
|
|
@@ -153,17 +167,20 @@ export class MySQLDump {
|
|
|
153
167
|
catch (inError) {
|
|
154
168
|
error = inError;
|
|
155
169
|
}
|
|
170
|
+
finally {
|
|
171
|
+
clearInterval(logIntervalId);
|
|
172
|
+
}
|
|
156
173
|
this.processes.push({
|
|
157
174
|
name: item.name,
|
|
158
175
|
error,
|
|
159
176
|
stats,
|
|
160
177
|
});
|
|
161
178
|
await this.ntfy.send("SQL dump", {
|
|
162
|
-
|
|
179
|
+
Name: item.name,
|
|
163
180
|
Size: formatBytes(stats.bytes),
|
|
164
181
|
Files: stats.files,
|
|
165
182
|
Duration: duration(Date.now() - now),
|
|
166
183
|
Error: error?.message,
|
|
167
|
-
}, error);
|
|
184
|
+
}, { error, logId });
|
|
168
185
|
}
|
|
169
186
|
}
|
package/lib/utils/ntfy.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare class Ntfy {
|
|
|
21
21
|
private formatTitle;
|
|
22
22
|
private formatMessage;
|
|
23
23
|
private formatMessageObject;
|
|
24
|
-
send(inTitle: string, message: MessageObject,
|
|
24
|
+
send(inTitle: string, message: MessageObject, options?: {
|
|
25
|
+
error?: Error | boolean;
|
|
26
|
+
logId?: string;
|
|
27
|
+
}): Promise<string | undefined>;
|
|
25
28
|
}
|
|
26
29
|
export {};
|
package/lib/utils/ntfy.js
CHANGED
|
@@ -38,31 +38,37 @@ export class Ntfy {
|
|
|
38
38
|
})
|
|
39
39
|
.join("\n");
|
|
40
40
|
}
|
|
41
|
-
async send(inTitle, message,
|
|
41
|
+
async send(inTitle, message, options = {}) {
|
|
42
42
|
const title = this.formatTitle(inTitle);
|
|
43
43
|
const body = this.formatMessageObject(message);
|
|
44
44
|
const lines = [title, body].filter((v) => v.length);
|
|
45
45
|
if (lines.length)
|
|
46
46
|
console.info([...lines, ""].join("\n"));
|
|
47
|
-
const
|
|
48
|
-
priority: error ? "high" : "default",
|
|
49
|
-
tags: [error ? "red_circle" : "green_circle"],
|
|
47
|
+
const ntfyOptions = {
|
|
48
|
+
priority: options.error ? "high" : "default",
|
|
49
|
+
tags: [options.error ? "red_circle" : "green_circle"],
|
|
50
50
|
};
|
|
51
51
|
try {
|
|
52
|
-
if (this.options.token)
|
|
53
|
-
|
|
52
|
+
if (this.options.token) {
|
|
53
|
+
const token = options.logId
|
|
54
|
+
? `${this.options.token}/${options.logId}`
|
|
55
|
+
: this.options.token;
|
|
56
|
+
const response = await fetch(`https://ntfy.sh/${token}`, {
|
|
54
57
|
dispatcher: this.agent,
|
|
55
58
|
method: "POST",
|
|
56
59
|
body: unstyle(body),
|
|
57
60
|
headers: {
|
|
58
61
|
Markdown: "yes",
|
|
59
62
|
Title: unstyle(title),
|
|
60
|
-
Priority:
|
|
61
|
-
...(
|
|
62
|
-
Tags:
|
|
63
|
+
Priority: ntfyOptions.priority ?? "default",
|
|
64
|
+
...(ntfyOptions.tags && {
|
|
65
|
+
Tags: ntfyOptions.tags?.join(","),
|
|
63
66
|
}),
|
|
64
67
|
},
|
|
65
68
|
});
|
|
69
|
+
const json = (await response.json());
|
|
70
|
+
return json.id;
|
|
71
|
+
}
|
|
66
72
|
await setTimeout(this.options.delay ?? 800);
|
|
67
73
|
}
|
|
68
74
|
catch (error) {
|
package/lib/utils/string.d.ts
CHANGED
package/lib/utils/string.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { randomInt } from "crypto";
|
|
1
2
|
export function unstyle(str) {
|
|
2
3
|
return str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
3
4
|
}
|
|
5
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
6
|
+
export function randomString(length) {
|
|
7
|
+
let string = "";
|
|
8
|
+
for (let i = 0; i < length; i++) {
|
|
9
|
+
const index = randomInt(0, charset.length);
|
|
10
|
+
string += charset[index];
|
|
11
|
+
}
|
|
12
|
+
return string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const prefix = "dt";
|
|
2
|
+
const prefixSep = "-";
|
|
3
|
+
const valueSep = ":";
|
|
4
|
+
const fullPrefix = `${prefix}${prefixSep}`;
|
|
5
|
+
export function stringifyTags(inTags) {
|
|
6
|
+
const tags = [];
|
|
7
|
+
for (const [key, value] of Object.entries(inTags)) {
|
|
8
|
+
const tagName = key === prefix ? key : `${prefix}${prefixSep}${key}`;
|
|
9
|
+
tags.push(value === true ? tagName : `${tagName}${valueSep}${value}`);
|
|
10
|
+
}
|
|
11
|
+
return tags;
|
|
12
|
+
}
|
|
13
|
+
export function parseTagPrefix(tag) {
|
|
14
|
+
if (tag === prefix) {
|
|
15
|
+
return tag;
|
|
16
|
+
}
|
|
17
|
+
else if (tag.startsWith(fullPrefix)) {
|
|
18
|
+
return tag.slice(fullPrefix.length);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function parseTags(inTags) {
|
|
22
|
+
const tags = {};
|
|
23
|
+
for (const tag of inTags) {
|
|
24
|
+
const str = parseTagPrefix(tag);
|
|
25
|
+
if (!str) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
else if (str.includes(valueSep)) {
|
|
29
|
+
const [tagName, tagValue] = str.split(valueSep);
|
|
30
|
+
tags[tagName] = tagValue ?? "";
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
tags[str] = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return tags;
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datatruck/restic",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Tool for creating and managing backups",
|
|
5
5
|
"homepage": "https://github.com/swordev/datatruck#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ajv": "^8.17.1",
|
|
35
35
|
"commander": "^14.0.2",
|
|
36
36
|
"undici": "^7.18.2",
|
|
37
|
-
"@datatruck/cli": "0.41.
|
|
37
|
+
"@datatruck/cli": "0.41.7"
|
|
38
38
|
},
|
|
39
39
|
"engine": {
|
|
40
40
|
"node": ">=20.0.0"
|