@datatruck/restic 0.0.3 → 0.0.5
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 +19 -24
- package/lib/actions/copy.d.ts +0 -1
- package/lib/actions/copy.js +3 -11
- package/lib/actions/prune.js +4 -10
- package/lib/actions/run.d.ts +8 -0
- package/lib/actions/run.js +16 -0
- package/lib/bin.d.ts +1 -0
- package/lib/bin.js +1 -0
- package/lib/create-bin.js +34 -0
- 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);
|
|
@@ -34,14 +32,19 @@ export class Backup extends Action {
|
|
|
34
32
|
minFreeSpace: this.config.minFreeSpace,
|
|
35
33
|
minFreeSpacePath: targetPath ?? process.cwd(),
|
|
36
34
|
targetPath,
|
|
37
|
-
rutine: () => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tags:
|
|
42
|
-
};
|
|
43
|
-
return restic.backup({
|
|
44
|
-
|
|
35
|
+
rutine: async () => {
|
|
36
|
+
const last = await restic.snapshots({
|
|
37
|
+
latest: 1,
|
|
38
|
+
host: this.config.hostname,
|
|
39
|
+
tags: stringifyTags({ pkg: pkg.name }),
|
|
40
|
+
});
|
|
41
|
+
return await restic.backup({
|
|
42
|
+
parent: last.at(0)?.id,
|
|
43
|
+
host: this.config.hostname,
|
|
44
|
+
tags: stringifyTags({
|
|
45
|
+
...tags,
|
|
46
|
+
pkg: pkg.name,
|
|
47
|
+
}),
|
|
45
48
|
paths: [pkg.path],
|
|
46
49
|
exclude: pkg.exclude,
|
|
47
50
|
onStream(data) {
|
|
@@ -55,9 +58,7 @@ export class Backup extends Action {
|
|
|
55
58
|
});
|
|
56
59
|
const snapshots = await restic.snapshots({
|
|
57
60
|
json: true,
|
|
58
|
-
tags:
|
|
59
|
-
ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
|
|
60
|
-
],
|
|
61
|
+
tags: stringifyTags({ pkg: pkg.name }),
|
|
61
62
|
});
|
|
62
63
|
snapshotsAmount = snapshots.length;
|
|
63
64
|
}).start(async (data) => {
|
|
@@ -101,14 +102,8 @@ export class Backup extends Action {
|
|
|
101
102
|
.filter((repo) => isLocalDir(repo.uri))
|
|
102
103
|
.map((repo) => repo.uri);
|
|
103
104
|
const tags = {
|
|
104
|
-
|
|
105
|
-
|
|
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",
|
|
105
|
+
dt: true,
|
|
106
|
+
id: randomString(8),
|
|
112
107
|
};
|
|
113
108
|
for (const task of tasks ?? []) {
|
|
114
109
|
if (task.type === "mysql-dump") {
|
|
@@ -181,7 +176,7 @@ export class Backup extends Action {
|
|
|
181
176
|
!!diskStats.result?.length && { key: "Disk stats", value: "" },
|
|
182
177
|
...(diskStats.result?.map((p) => ({
|
|
183
178
|
key: p.name,
|
|
184
|
-
value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
|
|
179
|
+
value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.total - p.free)}%)`,
|
|
185
180
|
level: 1,
|
|
186
181
|
})) || []),
|
|
187
182
|
!!sqlDumpProcesses.length && { key: "SQL Dumps", value: "" },
|
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,
|
package/lib/actions/prune.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
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";
|
|
@@ -24,9 +23,7 @@ export class Prune extends Action {
|
|
|
24
23
|
json: true,
|
|
25
24
|
prune: true,
|
|
26
25
|
args: ["--group-by", ""],
|
|
27
|
-
tag:
|
|
28
|
-
ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkgName),
|
|
29
|
-
],
|
|
26
|
+
tag: stringifyTags({ pkg: pkgName }),
|
|
30
27
|
});
|
|
31
28
|
stats = {
|
|
32
29
|
keep: results.at(0)?.keep?.length ?? 0,
|
|
@@ -61,10 +58,7 @@ export class Prune extends Action {
|
|
|
61
58
|
if (!(await restic.checkRepository()))
|
|
62
59
|
return [];
|
|
63
60
|
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
|
-
}));
|
|
61
|
+
const packages = new Set(snapshots.map((s) => parseTags(s.tags ?? []).pkg));
|
|
68
62
|
return [...packages].filter((v) => typeof v === "string");
|
|
69
63
|
}
|
|
70
64
|
async run(options) {
|
|
@@ -113,7 +107,7 @@ export class Prune extends Action {
|
|
|
113
107
|
!!diskStats.result?.length && { key: "Disk stats", value: "" },
|
|
114
108
|
...(diskStats.result?.map((p) => ({
|
|
115
109
|
key: p.name,
|
|
116
|
-
value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
|
|
110
|
+
value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.total - p.free)}%)`,
|
|
117
111
|
level: 1,
|
|
118
112
|
})) || []),
|
|
119
113
|
],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Action } from "./base.js";
|
|
2
|
+
import { logExec } from "@datatruck/cli/utils/cli.js";
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
export class Run extends Action {
|
|
5
|
+
async run(options) {
|
|
6
|
+
const [restic] = this.cm.createRestic(options.repository, this.verbose);
|
|
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);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/lib/bin.d.ts
CHANGED
package/lib/bin.js
CHANGED
package/lib/create-bin.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Copy } from "./actions/copy.js";
|
|
|
3
3
|
import { Create } from "./actions/create.js";
|
|
4
4
|
import { Init } from "./actions/init.js";
|
|
5
5
|
import { Prune } from "./actions/prune.js";
|
|
6
|
+
import { Run } from "./actions/run.js";
|
|
6
7
|
import { parseConfigFile } from "./config.js";
|
|
7
8
|
import { parseStringList } from "@datatruck/cli/utils/string.js";
|
|
8
9
|
import { Command } from "commander";
|
|
@@ -32,6 +33,21 @@ export function createBin(inConfig) {
|
|
|
32
33
|
const create = new Create();
|
|
33
34
|
await create.run(options);
|
|
34
35
|
});
|
|
36
|
+
program
|
|
37
|
+
.command("run")
|
|
38
|
+
.description("Run arbitrary restic command")
|
|
39
|
+
.option("-r, --repository <name>", "Repository name")
|
|
40
|
+
.argument("[args...]", "Restic arguments")
|
|
41
|
+
.allowUnknownOption()
|
|
42
|
+
.allowExcessArguments()
|
|
43
|
+
.action(async (args, options) => {
|
|
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;
|
|
48
|
+
const run = new Run(config, globalOptions);
|
|
49
|
+
await run.run({ repository, args });
|
|
50
|
+
});
|
|
35
51
|
program
|
|
36
52
|
.command("init")
|
|
37
53
|
.alias("i")
|
|
@@ -79,5 +95,23 @@ export function createBin(inConfig) {
|
|
|
79
95
|
const prune = new Prune(config, globalOptions);
|
|
80
96
|
await prune.run(options);
|
|
81
97
|
});
|
|
98
|
+
const parse = program.parse.bind(program);
|
|
99
|
+
program.parse = function (args, options) {
|
|
100
|
+
if (!args)
|
|
101
|
+
args = process.argv;
|
|
102
|
+
const [node, script, command, ...rest] = args;
|
|
103
|
+
if (command === "run") {
|
|
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
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parse(args, options);
|
|
115
|
+
};
|
|
82
116
|
return program;
|
|
83
117
|
}
|
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.5",
|
|
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"
|