@datatruck/restic 0.0.1 → 0.0.2
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/config.schema.json +136 -21
- package/lib/actions/backup.d.ts +38 -36
- package/lib/actions/backup.js +169 -112
- package/lib/actions/base.d.ts +10 -0
- package/lib/actions/base.js +19 -0
- package/lib/actions/copy.d.ts +17 -11
- package/lib/actions/copy.js +99 -109
- package/lib/actions/init.d.ts +3 -8
- package/lib/actions/init.js +34 -49
- package/lib/actions/prune.d.ts +10 -0
- package/lib/actions/prune.js +105 -0
- package/lib/bin.js +15 -0
- package/lib/config.d.ts +42 -6
- package/lib/config.js +31 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +1 -0
- package/lib/utils/async.d.ts +10 -0
- package/lib/utils/async.js +36 -0
- package/lib/utils/fs.d.ts +10 -1
- package/lib/utils/fs.js +39 -2
- package/lib/utils/mysql.d.ts +2 -8
- package/lib/utils/mysql.js +7 -10
- package/lib/utils/ntfy.d.ts +12 -4
- package/lib/utils/ntfy.js +36 -16
- package/lib/utils/string.d.ts +1 -0
- package/lib/utils/string.js +3 -0
- package/package.json +2 -2
- package/lib/utils/restic-backup.d.ts +0 -49
- package/lib/utils/restic-backup.js +0 -91
package/lib/actions/copy.js
CHANGED
|
@@ -1,129 +1,119 @@
|
|
|
1
|
+
import { createRunner } from "../utils/async.js";
|
|
1
2
|
import { checkDiskSpace } from "../utils/fs.js";
|
|
2
|
-
import {
|
|
3
|
+
import { Action } from "./base.js";
|
|
4
|
+
import { Prune } from "./prune.js";
|
|
3
5
|
import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
|
|
4
6
|
import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
|
|
5
7
|
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
6
|
-
import { duration } from "@datatruck/cli/utils/date.js";
|
|
7
8
|
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
8
9
|
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
9
|
-
export class Copy {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
token: this.config.ntfyToken,
|
|
20
|
-
titlePrefix: `[${this.config.hostname}] `,
|
|
10
|
+
export class Copy extends Action {
|
|
11
|
+
initializedRepos = new Set();
|
|
12
|
+
async findSnapshots(name, packages) {
|
|
13
|
+
const repo = this.cm.findRepository(name);
|
|
14
|
+
const restic = new Restic({
|
|
15
|
+
log: this.verbose,
|
|
16
|
+
env: {
|
|
17
|
+
RESTIC_REPOSITORY: repo.uri,
|
|
18
|
+
RESTIC_PASSWORD: repo.password,
|
|
19
|
+
},
|
|
21
20
|
});
|
|
21
|
+
const snapshots = (await restic.snapshots({
|
|
22
|
+
latest: 1,
|
|
23
|
+
group: ["path"],
|
|
24
|
+
tags: packages
|
|
25
|
+
? packages.map((name) => ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, name))
|
|
26
|
+
: undefined,
|
|
27
|
+
}));
|
|
28
|
+
return snapshots.flatMap((s) => s.snapshots);
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
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
|
+
async runSingle(options) {
|
|
37
|
+
const { snapshot } = options;
|
|
38
|
+
const targetRepo = this.cm.findRepository(options.target);
|
|
39
|
+
const sourceRepo = this.cm.findRepository(options.source);
|
|
40
|
+
const pkgTag = this.findPackageTag(snapshot.tags);
|
|
41
|
+
const packageName = pkgTag?.value;
|
|
42
|
+
const targetPath = isLocalDir(targetRepo.uri) ? targetRepo.uri : undefined;
|
|
43
|
+
const target = new Restic({
|
|
44
|
+
log: this.verbose,
|
|
45
|
+
env: {
|
|
46
|
+
RESTIC_REPOSITORY: targetRepo.uri,
|
|
47
|
+
RESTIC_PASSWORD: targetRepo.password,
|
|
48
|
+
["GODEBUG"]: "http2client=0",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
let space;
|
|
52
|
+
await createRunner(async () => {
|
|
53
|
+
if (!this.initializedRepos.has(targetRepo.name)) {
|
|
54
|
+
await target.tryInit();
|
|
55
|
+
this.initializedRepos.add(targetRepo.name);
|
|
56
|
+
}
|
|
57
|
+
space = await checkDiskSpace({
|
|
58
|
+
minFreeSpace: this.config.minFreeSpace,
|
|
59
|
+
targetPath,
|
|
60
|
+
rutine: () => target.copy({
|
|
61
|
+
ids: [snapshot.id],
|
|
62
|
+
fromRepo: sourceRepo.uri,
|
|
63
|
+
fromRepoPassword: sourceRepo.password,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}).start(async (data) => {
|
|
67
|
+
await this.ntfy.send(`Copy`, {
|
|
68
|
+
Id: snapshot.short_id,
|
|
69
|
+
Source: sourceRepo.name,
|
|
70
|
+
Target: targetRepo.name,
|
|
71
|
+
Package: packageName,
|
|
72
|
+
...(space !== undefined && {
|
|
73
|
+
Size: `${formatBytes(space.size)} (${formatBytes(space.diff, true)})`,
|
|
74
|
+
}),
|
|
75
|
+
Duration: data.duration,
|
|
76
|
+
Error: data.error?.message,
|
|
77
|
+
}, data.error);
|
|
78
|
+
});
|
|
79
|
+
return space?.diff ?? 0;
|
|
28
80
|
}
|
|
29
81
|
async run(options) {
|
|
30
|
-
const now = Date.now();
|
|
31
82
|
let globalDiffSize;
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
await createRunner(async () => {
|
|
84
|
+
const [source] = this.cm.filterRepositories([options.source]);
|
|
85
|
+
const targets = this.cm.filterRepositories(options.targets);
|
|
34
86
|
await this.ntfy.send(`Copy start`, {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
const sourceRepo = this.findRepo(options.source);
|
|
39
|
-
const source = new Restic({
|
|
40
|
-
log: this.verbose,
|
|
41
|
-
env: {
|
|
42
|
-
RESTIC_REPOSITORY: sourceRepo.uri,
|
|
43
|
-
RESTIC_PASSWORD: sourceRepo.password,
|
|
44
|
-
},
|
|
87
|
+
Source: source.name,
|
|
88
|
+
Targets: targets.map((t) => t.name).join(", "),
|
|
45
89
|
});
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
throw new Error(`No target repositories specified`);
|
|
49
|
-
const inSnapshots = (await source.snapshots({
|
|
50
|
-
latest: 1,
|
|
51
|
-
group: ["path"],
|
|
52
|
-
tags: options.packages
|
|
53
|
-
? options.packages.map((name) => ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, name))
|
|
54
|
-
: undefined,
|
|
55
|
-
}));
|
|
56
|
-
const snapshots = inSnapshots.flatMap((s) => s.snapshots);
|
|
57
|
-
for (const targetRepo of targetRepos) {
|
|
58
|
-
const target = new Restic({
|
|
59
|
-
log: this.verbose,
|
|
60
|
-
env: {
|
|
61
|
-
RESTIC_REPOSITORY: targetRepo.uri,
|
|
62
|
-
RESTIC_PASSWORD: targetRepo.password,
|
|
63
|
-
["GODEBUG"]: "http2client=0",
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
const exists = await target.checkRepository();
|
|
67
|
-
if (!exists && isLocalDir(targetRepo.uri))
|
|
68
|
-
await target.exec(["init"]);
|
|
90
|
+
const snapshots = await this.findSnapshots(source.name, options.packages);
|
|
91
|
+
for (const target of targets) {
|
|
69
92
|
for (const snapshot of snapshots) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
const packageName = pkgTag?.value;
|
|
75
|
-
const now = Date.now();
|
|
76
|
-
let copyError;
|
|
77
|
-
let diffSize;
|
|
78
|
-
const targetPath = isLocalDir(targetRepo.uri)
|
|
79
|
-
? targetRepo.uri
|
|
80
|
-
: undefined;
|
|
81
|
-
try {
|
|
82
|
-
diffSize = await checkDiskSpace({
|
|
83
|
-
minFreeSpace: this.config.minFreeSpace,
|
|
84
|
-
targetPath,
|
|
85
|
-
rutine: () => target.copy({
|
|
86
|
-
ids: [snapshot.id],
|
|
87
|
-
fromRepo: sourceRepo.uri,
|
|
88
|
-
fromRepoPassword: sourceRepo.password,
|
|
89
|
-
}),
|
|
90
|
-
});
|
|
91
|
-
if (diffSize !== undefined) {
|
|
92
|
-
globalDiffSize = (globalDiffSize ?? 0) + (diffSize ?? 0);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
catch (inError) {
|
|
96
|
-
copyError = inError;
|
|
97
|
-
}
|
|
98
|
-
await this.ntfy.send(`Copy`, {
|
|
99
|
-
"- Id": snapshot.short_id,
|
|
100
|
-
"- Source": sourceRepo.name,
|
|
101
|
-
"- Target": targetRepo.name,
|
|
102
|
-
"- Package": packageName,
|
|
103
|
-
...(diffSize !== undefined && {
|
|
104
|
-
"- Diff size": (diffSize > 0 ? "+" : "") + formatBytes(diffSize),
|
|
105
|
-
}),
|
|
106
|
-
"- Duration": duration(Date.now() - now),
|
|
107
|
-
"- Error": copyError?.message,
|
|
108
|
-
}, {
|
|
109
|
-
priority: copyError ? "high" : "default",
|
|
110
|
-
tags: [copyError ? "red_circle" : "green_circle"],
|
|
93
|
+
const diffSize = await this.runSingle({
|
|
94
|
+
snapshot,
|
|
95
|
+
source: source.name,
|
|
96
|
+
target: target.name,
|
|
111
97
|
});
|
|
98
|
+
globalDiffSize = (globalDiffSize ?? 0) + diffSize;
|
|
112
99
|
}
|
|
113
100
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
if (!options.targets.length)
|
|
102
|
+
throw new Error(`No target repositories specified`);
|
|
103
|
+
}).start(async (data) => {
|
|
104
|
+
await this.ntfy.send(`Copy end`, {
|
|
105
|
+
...(globalDiffSize !== undefined && {
|
|
106
|
+
"Diff size": formatBytes(globalDiffSize, true),
|
|
107
|
+
}),
|
|
108
|
+
Duration: data.duration,
|
|
109
|
+
Error: data.error?.message,
|
|
110
|
+
}, data.error);
|
|
111
|
+
if (options.prune) {
|
|
112
|
+
await new Prune(this.config, this.global).run({
|
|
113
|
+
packages: options.packages,
|
|
114
|
+
repositories: options.targets,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
127
117
|
});
|
|
128
118
|
}
|
|
129
119
|
}
|
package/lib/actions/init.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Ntfy } from "../utils/ntfy.js";
|
|
1
|
+
import { Action } from "./base.js";
|
|
3
2
|
export type InitOptions = {
|
|
4
3
|
repositories?: string[];
|
|
5
4
|
};
|
|
6
|
-
export declare class Init {
|
|
7
|
-
|
|
8
|
-
readonly global?: GlobalConfig | undefined;
|
|
9
|
-
readonly ntfy: Ntfy;
|
|
10
|
-
protected verbose: boolean | undefined;
|
|
11
|
-
constructor(config: Config, global?: GlobalConfig | undefined);
|
|
5
|
+
export declare class Init extends Action {
|
|
6
|
+
protected runSingle(name: string): Promise<void>;
|
|
12
7
|
run(options: InitOptions): Promise<void>;
|
|
13
8
|
}
|
package/lib/actions/init.js
CHANGED
|
@@ -1,56 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
1
|
+
import { createRunner } from "../utils/async.js";
|
|
2
|
+
import { Action } from "./base.js";
|
|
4
3
|
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
5
|
-
export class Init {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
export class Init extends Action {
|
|
5
|
+
async runSingle(name) {
|
|
6
|
+
let exists;
|
|
7
|
+
await createRunner(async () => {
|
|
8
|
+
const repo = this.cm.findRepository(name);
|
|
9
|
+
const restic = new Restic({
|
|
10
|
+
log: this.verbose,
|
|
11
|
+
env: {
|
|
12
|
+
RESTIC_REPOSITORY: repo.uri,
|
|
13
|
+
RESTIC_PASSWORD: repo.password,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
exists = await restic.tryInit();
|
|
17
|
+
}).start(async (data) => {
|
|
18
|
+
await this.ntfy.send(`Init`, {
|
|
19
|
+
Repository: name,
|
|
20
|
+
Exists: exists ? "yes" : "no",
|
|
21
|
+
Duration: data.duration,
|
|
22
|
+
Error: data.error?.message,
|
|
23
|
+
}, data.error);
|
|
17
24
|
});
|
|
18
25
|
}
|
|
19
26
|
async run(options) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let error;
|
|
25
|
-
let exists;
|
|
26
|
-
try {
|
|
27
|
-
const repo = this.config.repositories.find((repo) => repo.name === name);
|
|
28
|
-
if (!repo)
|
|
29
|
-
throw new Error(`Repository not found`);
|
|
30
|
-
const source = new Restic({
|
|
31
|
-
log: this.verbose,
|
|
32
|
-
env: {
|
|
33
|
-
RESTIC_REPOSITORY: repo.uri,
|
|
34
|
-
RESTIC_PASSWORD: repo.password,
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
exists = await source.checkRepository();
|
|
38
|
-
if (!exists && isLocalDir(repo.uri))
|
|
39
|
-
await source.exec(["init"]);
|
|
40
|
-
}
|
|
41
|
-
catch (inError) {
|
|
42
|
-
error = inError;
|
|
43
|
-
}
|
|
44
|
-
await this.ntfy.send(`Init`, {
|
|
45
|
-
"- Repository": name,
|
|
46
|
-
"- Exists": exists ? "yes" : "no",
|
|
47
|
-
"- Duration": duration(Date.now() - now),
|
|
48
|
-
"- Error": error?.message,
|
|
49
|
-
}, {
|
|
50
|
-
priority: error ? "high" : "default",
|
|
51
|
-
tags: [error ? "red_circle" : "green_circle"],
|
|
27
|
+
await createRunner(async () => {
|
|
28
|
+
const repositories = this.cm.filterRepositories(options.repositories);
|
|
29
|
+
await this.ntfy.send(`Init start`, {
|
|
30
|
+
Repositories: repositories.length,
|
|
52
31
|
});
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
for (const repo of repositories)
|
|
33
|
+
await this.runSingle(repo.name);
|
|
34
|
+
}).start(async (data) => {
|
|
35
|
+
await this.ntfy.send(`Init end`, {
|
|
36
|
+
Duration: data.duration,
|
|
37
|
+
Error: data.error?.message,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
55
40
|
}
|
|
56
41
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PrunePolicy } from "../config.js";
|
|
2
|
+
import { Action } from "./base.js";
|
|
3
|
+
export type PruneOptions = {
|
|
4
|
+
packages?: string[];
|
|
5
|
+
repositories?: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare class Prune extends Action {
|
|
8
|
+
protected runSingle(repoName: string, pkgName: string, policy: PrunePolicy): Promise<number>;
|
|
9
|
+
run(options: PruneOptions): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRunner, safeRun } from "../utils/async.js";
|
|
2
|
+
import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
|
|
3
|
+
import { Action } from "./base.js";
|
|
4
|
+
import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
|
|
5
|
+
import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
|
|
6
|
+
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
7
|
+
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
8
|
+
import { progressPercent } from "@datatruck/cli/utils/math.js";
|
|
9
|
+
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
10
|
+
export class Prune extends Action {
|
|
11
|
+
async runSingle(repoName, pkgName, policy) {
|
|
12
|
+
let stats;
|
|
13
|
+
let space;
|
|
14
|
+
await createRunner(async () => {
|
|
15
|
+
const repo = this.cm.findRepository(repoName);
|
|
16
|
+
const pkg = this.cm.findPackage(pkgName);
|
|
17
|
+
const restic = new Restic({
|
|
18
|
+
log: this.verbose,
|
|
19
|
+
env: {
|
|
20
|
+
RESTIC_REPOSITORY: repo.uri,
|
|
21
|
+
RESTIC_PASSWORD: repo.password,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
|
|
25
|
+
space = await checkDiskSpace({
|
|
26
|
+
minFreeSpace: this.config.minFreeSpace,
|
|
27
|
+
targetPath,
|
|
28
|
+
rutine: async () => {
|
|
29
|
+
const results = await restic.forget({
|
|
30
|
+
...policy,
|
|
31
|
+
json: true,
|
|
32
|
+
prune: true,
|
|
33
|
+
args: ["--group-by", ""],
|
|
34
|
+
tag: [
|
|
35
|
+
ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
stats = {
|
|
39
|
+
keep: results.at(0)?.keep?.length ?? 0,
|
|
40
|
+
remove: results.at(0)?.remove?.length ?? 0,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}).start(async (data) => {
|
|
45
|
+
await this.ntfy.send(`Prune`, {
|
|
46
|
+
Repository: repoName,
|
|
47
|
+
Package: pkgName,
|
|
48
|
+
...(stats && {
|
|
49
|
+
Snapshots: `${stats.keep}`,
|
|
50
|
+
Removed: `${stats.remove}`,
|
|
51
|
+
}),
|
|
52
|
+
...(space !== undefined && {
|
|
53
|
+
Size: `${formatBytes(space.size)} (${formatBytes(space.diff, true)})`,
|
|
54
|
+
}),
|
|
55
|
+
Duration: data.duration,
|
|
56
|
+
Error: data.error?.message,
|
|
57
|
+
}, data.error);
|
|
58
|
+
});
|
|
59
|
+
return space?.diff ?? 0;
|
|
60
|
+
}
|
|
61
|
+
async run(options) {
|
|
62
|
+
let globalDiffSize;
|
|
63
|
+
let localRepositoryPaths = [];
|
|
64
|
+
await createRunner(async () => {
|
|
65
|
+
const repositories = this.cm.filterRepositories(options.repositories);
|
|
66
|
+
const packages = this.cm.filterPackages(options.packages);
|
|
67
|
+
await this.ntfy.send(`Prune start`, {
|
|
68
|
+
Repositories: repositories.length,
|
|
69
|
+
Packages: packages.length,
|
|
70
|
+
});
|
|
71
|
+
localRepositoryPaths = repositories
|
|
72
|
+
.filter((repo) => isLocalDir(repo.uri))
|
|
73
|
+
.map((repo) => repo.uri);
|
|
74
|
+
for (const repo of repositories) {
|
|
75
|
+
for (const pkg of packages) {
|
|
76
|
+
const policy = pkg.prunePolicy ?? repo.prunePolicy ?? this.config.prunePolicy;
|
|
77
|
+
if (policy) {
|
|
78
|
+
const diffSize = await this.runSingle(repo.name, pkg.name, policy);
|
|
79
|
+
if (diffSize !== undefined)
|
|
80
|
+
globalDiffSize = (globalDiffSize ?? 0) + diffSize;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}).start(async (data) => {
|
|
85
|
+
const diskStats = await safeRun(() => fetchMultipleDiskStats(localRepositoryPaths));
|
|
86
|
+
if (diskStats.error)
|
|
87
|
+
console.error(diskStats.error);
|
|
88
|
+
await this.ntfy.send(`Prune end`, {
|
|
89
|
+
Duration: data.duration,
|
|
90
|
+
...(globalDiffSize !== undefined && {
|
|
91
|
+
"Diff size": (globalDiffSize > 0 ? "+" : "") + formatBytes(globalDiffSize),
|
|
92
|
+
}),
|
|
93
|
+
Error: data.error?.message,
|
|
94
|
+
"": [
|
|
95
|
+
!!diskStats.result?.length && { key: "Disk stats", value: "" },
|
|
96
|
+
...(diskStats.result?.map((p) => ({
|
|
97
|
+
key: p.name,
|
|
98
|
+
value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
|
|
99
|
+
level: 1,
|
|
100
|
+
})) || []),
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
package/lib/bin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Backup } from "./actions/backup.js";
|
|
2
2
|
import { Copy } from "./actions/copy.js";
|
|
3
3
|
import { Init } from "./actions/init.js";
|
|
4
|
+
import { Prune } from "./actions/prune.js";
|
|
4
5
|
import { parseConfigFile } from "./config.js";
|
|
5
6
|
import { parseStringList } from "@datatruck/cli/utils/string.js";
|
|
6
7
|
import { program } from "commander";
|
|
@@ -37,6 +38,7 @@ program
|
|
|
37
38
|
.description("Run backup action")
|
|
38
39
|
.option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
|
|
39
40
|
.option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
|
|
41
|
+
.option("--prune", "Prune after backup")
|
|
40
42
|
.action(async (options) => {
|
|
41
43
|
const { config, globalOptions } = await load();
|
|
42
44
|
const backup = new Backup(config, globalOptions);
|
|
@@ -49,9 +51,22 @@ program
|
|
|
49
51
|
.option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
|
|
50
52
|
.requiredOption("-s, --source <name>", "Source repository name")
|
|
51
53
|
.requiredOption("-t, --targets <names>", "Target repository names", (v) => parseStringList(v))
|
|
54
|
+
.option("--prune", "Prune after copy")
|
|
52
55
|
.action(async (options) => {
|
|
53
56
|
const { config, globalOptions } = await load();
|
|
54
57
|
const copy = new Copy(config, globalOptions);
|
|
55
58
|
await copy.run(options);
|
|
56
59
|
});
|
|
60
|
+
program
|
|
61
|
+
.command("prune")
|
|
62
|
+
.alias("p")
|
|
63
|
+
.description("Run prune action")
|
|
64
|
+
.option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
|
|
65
|
+
.option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
|
|
66
|
+
.option("--prune", "Prune after copy")
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
const { config, globalOptions } = await load();
|
|
69
|
+
const prune = new Prune(config, globalOptions);
|
|
70
|
+
await prune.run(options);
|
|
71
|
+
});
|
|
57
72
|
program.parse();
|
package/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MySQLDumpOptions } from "./utils/mysql.js";
|
|
1
2
|
export type GlobalConfig = {
|
|
2
3
|
config?: string;
|
|
3
4
|
verbose?: boolean;
|
|
@@ -8,6 +9,7 @@ export type Config = {
|
|
|
8
9
|
ntfyToken?: string;
|
|
9
10
|
minFreeSpace?: string;
|
|
10
11
|
verbose?: boolean;
|
|
12
|
+
prunePolicy?: PrunePolicy;
|
|
11
13
|
tasks?: {
|
|
12
14
|
type: "mysql-dump";
|
|
13
15
|
packages: string[];
|
|
@@ -20,26 +22,60 @@ export type Config = {
|
|
|
20
22
|
path: string | false;
|
|
21
23
|
}[] | string;
|
|
22
24
|
concurrency?: number;
|
|
23
|
-
connection:
|
|
24
|
-
hostname: string;
|
|
25
|
-
username: string;
|
|
26
|
-
password: string;
|
|
27
|
-
database?: string;
|
|
28
|
-
};
|
|
25
|
+
connection: MySQLDumpOptions["connection"];
|
|
29
26
|
};
|
|
30
27
|
}[];
|
|
31
28
|
packages: {
|
|
32
29
|
name: string;
|
|
33
30
|
path: string;
|
|
34
31
|
exclude?: string[];
|
|
32
|
+
prunePolicy?: PrunePolicy;
|
|
35
33
|
}[];
|
|
36
34
|
repositories: {
|
|
37
35
|
name: string;
|
|
38
36
|
password: string;
|
|
39
37
|
uri: string;
|
|
38
|
+
prunePolicy?: PrunePolicy;
|
|
40
39
|
}[];
|
|
41
40
|
};
|
|
41
|
+
export type PrunePolicy = {
|
|
42
|
+
keepMinutely?: number;
|
|
43
|
+
keepDaily?: number;
|
|
44
|
+
keepHourly?: number;
|
|
45
|
+
keepLast?: number;
|
|
46
|
+
keepMonthly?: number;
|
|
47
|
+
keepWeekly?: number;
|
|
48
|
+
keepYearly?: number;
|
|
49
|
+
};
|
|
42
50
|
export declare function defineConfig(config: Config): Config;
|
|
43
51
|
export declare function validateConfig(config: unknown): Promise<void>;
|
|
44
52
|
export declare function readConfigSchemaFile(): Promise<unknown>;
|
|
45
53
|
export declare function parseConfigFile(path?: string): Promise<Config>;
|
|
54
|
+
export declare class ConfigManager {
|
|
55
|
+
readonly config: Config;
|
|
56
|
+
constructor(config: Config);
|
|
57
|
+
filterRepositories(filter: string[] | undefined): {
|
|
58
|
+
name: string;
|
|
59
|
+
password: string;
|
|
60
|
+
uri: string;
|
|
61
|
+
prunePolicy?: PrunePolicy;
|
|
62
|
+
}[];
|
|
63
|
+
filterPackages(filter: string[] | undefined): {
|
|
64
|
+
name: string;
|
|
65
|
+
path: string;
|
|
66
|
+
exclude?: string[];
|
|
67
|
+
prunePolicy?: PrunePolicy;
|
|
68
|
+
}[];
|
|
69
|
+
findRepository(name: string): {
|
|
70
|
+
name: string;
|
|
71
|
+
password: string;
|
|
72
|
+
uri: string;
|
|
73
|
+
prunePolicy?: PrunePolicy;
|
|
74
|
+
};
|
|
75
|
+
findPackage(name: string): {
|
|
76
|
+
name: string;
|
|
77
|
+
path: string;
|
|
78
|
+
exclude?: string[];
|
|
79
|
+
prunePolicy?: PrunePolicy;
|
|
80
|
+
};
|
|
81
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseJSONFile } from "./utils/fs.js";
|
|
2
|
+
import { match } from "@datatruck/cli/utils/string.js";
|
|
2
3
|
import { Ajv } from "ajv";
|
|
3
4
|
export function defineConfig(config) {
|
|
4
5
|
return config;
|
|
@@ -20,3 +21,33 @@ export async function parseConfigFile(path = "datatruck.restic.json") {
|
|
|
20
21
|
await validateConfig(config);
|
|
21
22
|
return config;
|
|
22
23
|
}
|
|
24
|
+
export class ConfigManager {
|
|
25
|
+
config;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
filterRepositories(filter) {
|
|
30
|
+
const repositories = this.config.repositories.filter((v) => filter ? match(v.name, filter) : true);
|
|
31
|
+
if (!repositories.length)
|
|
32
|
+
throw new Error(`No repositories found for filter: ${filter?.join(", ")}`);
|
|
33
|
+
return repositories;
|
|
34
|
+
}
|
|
35
|
+
filterPackages(filter) {
|
|
36
|
+
const packages = this.config.packages.filter((v) => filter ? match(v.name, filter) : true);
|
|
37
|
+
if (!packages.length)
|
|
38
|
+
throw new Error(`No packages found for filter: ${filter?.join(", ")}`);
|
|
39
|
+
return packages;
|
|
40
|
+
}
|
|
41
|
+
findRepository(name) {
|
|
42
|
+
const repo = this.config.repositories.find((repo) => repo.name === name);
|
|
43
|
+
if (!repo)
|
|
44
|
+
throw new Error(`Repository '${name}' not found`);
|
|
45
|
+
return repo;
|
|
46
|
+
}
|
|
47
|
+
findPackage(name) {
|
|
48
|
+
const pkg = this.config.packages.find((pkg) => pkg.name === name);
|
|
49
|
+
if (!pkg)
|
|
50
|
+
throw new Error(`Package '${name}' not found`);
|
|
51
|
+
return pkg;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { Backup, type
|
|
2
|
-
export { Copy, type
|
|
1
|
+
export { Backup, type BackupOptions } from "./actions/backup.js";
|
|
2
|
+
export { Copy, type CopyOptions } from "./actions/copy.js";
|
|
3
3
|
export { Init, type InitOptions } from "./actions/init.js";
|
|
4
|
+
export { Prune, type PruneOptions } from "./actions/prune.js";
|
|
4
5
|
export { type Config, type GlobalConfig, parseConfigFile, defineConfig, validateConfig, } from "./config.js";
|
package/lib/index.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function createRunner(rutine: () => any): {
|
|
2
|
+
start: <R = any>(cb: (data: {
|
|
3
|
+
error: Error | undefined;
|
|
4
|
+
duration: string;
|
|
5
|
+
}) => Promise<R>) => Promise<R>;
|
|
6
|
+
};
|
|
7
|
+
export declare function safeRun<T>(cb: () => Promise<T>): Promise<{
|
|
8
|
+
error: Error | undefined;
|
|
9
|
+
result: T | undefined;
|
|
10
|
+
}>;
|