@datatruck/restic 0.0.1 → 0.0.3
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 +179 -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/create.d.ts +10 -0
- package/lib/actions/create.js +49 -0
- package/lib/actions/init.d.ts +3 -8
- package/lib/actions/init.js +34 -49
- package/lib/actions/prune.d.ts +14 -0
- package/lib/actions/prune.js +123 -0
- package/lib/bin.js +2 -57
- package/lib/config.d.ts +49 -6
- package/lib/config.js +47 -0
- package/lib/create-bin.d.ts +3 -0
- package/lib/create-bin.js +83 -0
- package/lib/index.d.ts +5 -3
- package/lib/index.js +2 -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
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
2
|
+
import { fetchDiskStats } from "@datatruck/cli/utils/fs.js";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { writeFile } from "fs/promises";
|
|
5
|
+
import { hostname } from "os";
|
|
6
|
+
import { join, relative } from "path";
|
|
7
|
+
export class Create {
|
|
8
|
+
constructor() { }
|
|
9
|
+
random() {
|
|
10
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
11
|
+
}
|
|
12
|
+
async run(options) {
|
|
13
|
+
const stats = await fetchDiskStats(options.cwd);
|
|
14
|
+
const minFreeSpaceBytes = (stats.total * 25) / 100;
|
|
15
|
+
const minFreeSpace = formatBytes(minFreeSpaceBytes);
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
const config = {
|
|
18
|
+
$schema: "https://unpkg.com/@datatruck/restic/config.schema.json",
|
|
19
|
+
hostname: hostname(),
|
|
20
|
+
minFreeSpace,
|
|
21
|
+
ntfyToken: this.random(),
|
|
22
|
+
packages: [
|
|
23
|
+
{
|
|
24
|
+
name: "default",
|
|
25
|
+
path: join(cwd, "data").replaceAll("\\", "/"),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
prunePolicy: {
|
|
29
|
+
keepDaily: 7,
|
|
30
|
+
keepMonthly: 12,
|
|
31
|
+
keepYearly: 5,
|
|
32
|
+
},
|
|
33
|
+
repositories: [
|
|
34
|
+
{
|
|
35
|
+
name: "default",
|
|
36
|
+
uri: join(cwd, "repo").replaceAll("\\", "/"),
|
|
37
|
+
password: this.random(),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const configPath = join(cwd, options.config);
|
|
42
|
+
if (existsSync(configPath) && !options.force)
|
|
43
|
+
throw new Error(`Config file already exists at path: ${configPath}`);
|
|
44
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
45
|
+
const relativeConfigPath = relative(cwd, configPath);
|
|
46
|
+
console.log(`- Created config file at path: ${relativeConfigPath}`);
|
|
47
|
+
console.info(`- Show log remotly via ntfy: https://ntfy.sh/${config.ntfyToken}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
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,14 @@
|
|
|
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<{
|
|
9
|
+
diffSize: number;
|
|
10
|
+
removed: number | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
protected fetchPackages(repoName: string): Promise<string[]>;
|
|
13
|
+
run(options: PruneOptions): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { match } from "@datatruck/cli/utils/string.js";
|
|
10
|
+
export class Prune extends Action {
|
|
11
|
+
async runSingle(repoName, pkgName, policy) {
|
|
12
|
+
let stats;
|
|
13
|
+
let space;
|
|
14
|
+
let removed;
|
|
15
|
+
await createRunner(async () => {
|
|
16
|
+
const [restic, repo] = this.cm.createRestic(repoName, this.verbose);
|
|
17
|
+
const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
|
|
18
|
+
space = await checkDiskSpace({
|
|
19
|
+
minFreeSpace: this.config.minFreeSpace,
|
|
20
|
+
targetPath,
|
|
21
|
+
rutine: async () => {
|
|
22
|
+
const results = await restic.forget({
|
|
23
|
+
...policy,
|
|
24
|
+
json: true,
|
|
25
|
+
prune: true,
|
|
26
|
+
args: ["--group-by", ""],
|
|
27
|
+
tag: [
|
|
28
|
+
ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkgName),
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
stats = {
|
|
32
|
+
keep: results.at(0)?.keep?.length ?? 0,
|
|
33
|
+
remove: results.at(0)?.remove?.length ?? 0,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}).start(async (data) => {
|
|
38
|
+
if (stats)
|
|
39
|
+
removed = stats.remove;
|
|
40
|
+
await this.ntfy.send(`Prune`, {
|
|
41
|
+
Repository: repoName,
|
|
42
|
+
Package: pkgName,
|
|
43
|
+
...(stats && {
|
|
44
|
+
Keeped: `${stats.keep}`,
|
|
45
|
+
Removed: `${stats.remove}`,
|
|
46
|
+
}),
|
|
47
|
+
...(space !== undefined && {
|
|
48
|
+
Size: `${formatBytes(space.size)} (${formatBytes(space.diff, true)})`,
|
|
49
|
+
}),
|
|
50
|
+
Duration: data.duration,
|
|
51
|
+
Error: data.error?.message,
|
|
52
|
+
}, data.error);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
diffSize: space?.diff ?? 0,
|
|
56
|
+
removed,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async fetchPackages(repoName) {
|
|
60
|
+
const [restic] = this.cm.createRestic(repoName, this.verbose);
|
|
61
|
+
if (!(await restic.checkRepository()))
|
|
62
|
+
return [];
|
|
63
|
+
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
|
+
}));
|
|
68
|
+
return [...packages].filter((v) => typeof v === "string");
|
|
69
|
+
}
|
|
70
|
+
async run(options) {
|
|
71
|
+
let globalDiffSize;
|
|
72
|
+
let globalRemoved = 0;
|
|
73
|
+
let localRepositoryPaths = [];
|
|
74
|
+
await createRunner(async () => {
|
|
75
|
+
const repositories = this.cm.filterRepositories(options.repositories);
|
|
76
|
+
await this.ntfy.send(`Prune start`, {
|
|
77
|
+
Repositories: repositories.length,
|
|
78
|
+
});
|
|
79
|
+
localRepositoryPaths = repositories
|
|
80
|
+
.filter((repo) => isLocalDir(repo.uri))
|
|
81
|
+
.map((repo) => repo.uri);
|
|
82
|
+
let some = false;
|
|
83
|
+
for (const repo of repositories) {
|
|
84
|
+
const inPackageNames = await this.fetchPackages(repo.name);
|
|
85
|
+
const packageNames = inPackageNames.filter((pkgName) => options.packages ? match(pkgName, options.packages) : true);
|
|
86
|
+
for (const pkgName of packageNames) {
|
|
87
|
+
const pkg = this.config.packages.find((pkg) => pkg.name === pkgName);
|
|
88
|
+
const policy = pkg?.prunePolicy ?? repo.prunePolicy ?? this.config.prunePolicy;
|
|
89
|
+
if (policy) {
|
|
90
|
+
const { diffSize, removed } = await this.runSingle(repo.name, pkgName, policy);
|
|
91
|
+
some = true;
|
|
92
|
+
if (diffSize !== undefined)
|
|
93
|
+
globalDiffSize = (globalDiffSize ?? 0) + diffSize;
|
|
94
|
+
if (removed !== undefined)
|
|
95
|
+
globalRemoved += removed;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (options.packages && !some)
|
|
100
|
+
throw new Error(`No packages found for filter: ${options.packages.join(", ")}`);
|
|
101
|
+
}).start(async (data) => {
|
|
102
|
+
const diskStats = await safeRun(() => fetchMultipleDiskStats(localRepositoryPaths));
|
|
103
|
+
if (diskStats.error)
|
|
104
|
+
console.error(diskStats.error);
|
|
105
|
+
await this.ntfy.send(`Prune end`, {
|
|
106
|
+
Duration: data.duration,
|
|
107
|
+
Removed: globalRemoved,
|
|
108
|
+
...(globalDiffSize !== undefined && {
|
|
109
|
+
"Diff size": (globalDiffSize > 0 ? "+" : "") + formatBytes(globalDiffSize),
|
|
110
|
+
}),
|
|
111
|
+
Error: data.error?.message,
|
|
112
|
+
"": [
|
|
113
|
+
!!diskStats.result?.length && { key: "Disk stats", value: "" },
|
|
114
|
+
...(diskStats.result?.map((p) => ({
|
|
115
|
+
key: p.name,
|
|
116
|
+
value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
|
|
117
|
+
level: 1,
|
|
118
|
+
})) || []),
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
package/lib/bin.js
CHANGED
|
@@ -1,57 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { Init } from "./actions/init.js";
|
|
4
|
-
import { parseConfigFile } from "./config.js";
|
|
5
|
-
import { parseStringList } from "@datatruck/cli/utils/string.js";
|
|
6
|
-
import { program } from "commander";
|
|
7
|
-
import { resolve } from "path";
|
|
8
|
-
function getGlobalOptions() {
|
|
9
|
-
const options = program.opts();
|
|
10
|
-
return {
|
|
11
|
-
...options,
|
|
12
|
-
config: resolve(options.config),
|
|
13
|
-
verbose: process.env.DEBUG ? true : options.verbose,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
async function load() {
|
|
17
|
-
const globalOptions = getGlobalOptions();
|
|
18
|
-
const config = await parseConfigFile(globalOptions.config);
|
|
19
|
-
return { globalOptions, config };
|
|
20
|
-
}
|
|
21
|
-
program
|
|
22
|
-
.option("-v, --verbose")
|
|
23
|
-
.option("-c, --config <path>", "Path to config file", "datatruck.restic.json");
|
|
24
|
-
program
|
|
25
|
-
.command("init")
|
|
26
|
-
.alias("i")
|
|
27
|
-
.description("Run init action")
|
|
28
|
-
.option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
|
|
29
|
-
.action(async (options) => {
|
|
30
|
-
const { config, globalOptions } = await load();
|
|
31
|
-
const init = new Init(config, globalOptions);
|
|
32
|
-
await init.run(options);
|
|
33
|
-
});
|
|
34
|
-
program
|
|
35
|
-
.command("backup")
|
|
36
|
-
.alias("b")
|
|
37
|
-
.description("Run backup action")
|
|
38
|
-
.option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
|
|
39
|
-
.option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
|
|
40
|
-
.action(async (options) => {
|
|
41
|
-
const { config, globalOptions } = await load();
|
|
42
|
-
const backup = new Backup(config, globalOptions);
|
|
43
|
-
await backup.run(options);
|
|
44
|
-
});
|
|
45
|
-
program
|
|
46
|
-
.command("copy")
|
|
47
|
-
.alias("c")
|
|
48
|
-
.description("Run copy action")
|
|
49
|
-
.option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
|
|
50
|
-
.requiredOption("-s, --source <name>", "Source repository name")
|
|
51
|
-
.requiredOption("-t, --targets <names>", "Target repository names", (v) => parseStringList(v))
|
|
52
|
-
.action(async (options) => {
|
|
53
|
-
const { config, globalOptions } = await load();
|
|
54
|
-
const copy = new Copy(config, globalOptions);
|
|
55
|
-
await copy.run(options);
|
|
56
|
-
});
|
|
57
|
-
program.parse();
|
|
1
|
+
import { createBin } from "./create-bin.js";
|
|
2
|
+
createBin().parse();
|