@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { duration } from "@datatruck/cli/utils/date.js";
|
|
2
|
+
export function createRunner(rutine) {
|
|
3
|
+
return {
|
|
4
|
+
start: async (cb) => {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
let error;
|
|
7
|
+
try {
|
|
8
|
+
await rutine();
|
|
9
|
+
}
|
|
10
|
+
catch (inError) {
|
|
11
|
+
error = inError;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return await cb({
|
|
15
|
+
duration: duration(Date.now() - now),
|
|
16
|
+
error,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (error)
|
|
21
|
+
console.error(error, "\n");
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function safeRun(cb) {
|
|
27
|
+
try {
|
|
28
|
+
return {
|
|
29
|
+
error: undefined,
|
|
30
|
+
result: await cb(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return { error: error, result: undefined };
|
|
35
|
+
}
|
|
36
|
+
}
|
package/lib/utils/fs.d.ts
CHANGED
|
@@ -4,4 +4,13 @@ export declare function checkDiskSpace(options: {
|
|
|
4
4
|
minFreeSpacePath?: string | undefined;
|
|
5
5
|
targetPath?: string | undefined;
|
|
6
6
|
rutine: () => any;
|
|
7
|
-
}): Promise<
|
|
7
|
+
}): Promise<{
|
|
8
|
+
diff: number;
|
|
9
|
+
size: number;
|
|
10
|
+
} | undefined>;
|
|
11
|
+
export declare function getDiskName(inPath: string): Promise<string>;
|
|
12
|
+
export declare function fetchMultipleDiskStats(paths: string[]): Promise<{
|
|
13
|
+
name: string;
|
|
14
|
+
free: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}[]>;
|
package/lib/utils/fs.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ensureFreeDiskSpace, fastFolderSizeAsync, fetchDiskStats, } from "@datatruck/cli/utils/fs.js";
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { mkdir, readFile, stat } from "fs/promises";
|
|
4
|
+
import { platform } from "os";
|
|
5
|
+
import { parse, resolve } from "path";
|
|
3
6
|
export async function parseJSONFile(path) {
|
|
4
7
|
const buffer = await readFile(path);
|
|
5
8
|
return JSON.parse(buffer.toString());
|
|
@@ -19,5 +22,39 @@ export async function checkDiskSpace(options) {
|
|
|
19
22
|
const prev = await fastFolderSizeAsync(options.targetPath);
|
|
20
23
|
await options.rutine();
|
|
21
24
|
const next = await fastFolderSizeAsync(options.targetPath);
|
|
22
|
-
return
|
|
25
|
+
return {
|
|
26
|
+
diff: next - prev,
|
|
27
|
+
size: next,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function getDiskName(inPath) {
|
|
31
|
+
const path = resolve(inPath);
|
|
32
|
+
if (platform() === "win32") {
|
|
33
|
+
return parse(path).root[0];
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return execSync(`df "${path}"`).toString().split("\n")[1].split(/\s+/)[0];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function fetchMultipleDiskStats(paths) {
|
|
40
|
+
for (const path of paths) {
|
|
41
|
+
await mkdir(path, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
const devices = {};
|
|
44
|
+
for (const inPath of paths) {
|
|
45
|
+
const path = resolve(inPath);
|
|
46
|
+
const info = await stat(path);
|
|
47
|
+
if (!devices[info.dev])
|
|
48
|
+
devices[info.dev] = path;
|
|
49
|
+
}
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const dev in devices) {
|
|
52
|
+
const path = devices[dev];
|
|
53
|
+
const stats = await fetchDiskStats(path);
|
|
54
|
+
result.push({
|
|
55
|
+
name: await getDiskName(path),
|
|
56
|
+
...stats,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
23
60
|
}
|
package/lib/utils/mysql.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ntfy } from "./ntfy.js";
|
|
2
|
-
import { createMysqlCli } from "@datatruck/cli/utils/mysql.js";
|
|
2
|
+
import { createMysqlCli, MysqlCliOptions } from "@datatruck/cli/utils/mysql.js";
|
|
3
3
|
export type MySQLDumpItem = {
|
|
4
4
|
name: string;
|
|
5
5
|
database: string;
|
|
@@ -13,13 +13,7 @@ export type MySQLDumpOptions = {
|
|
|
13
13
|
verbose?: boolean;
|
|
14
14
|
minFreeSpace?: string;
|
|
15
15
|
concurrency?: number;
|
|
16
|
-
connection:
|
|
17
|
-
};
|
|
18
|
-
export type MySQLDumpConnection = {
|
|
19
|
-
hostname: string;
|
|
20
|
-
password: string;
|
|
21
|
-
username: string;
|
|
22
|
-
port?: number;
|
|
16
|
+
connection: Omit<MysqlCliOptions, "verbose">;
|
|
23
17
|
};
|
|
24
18
|
export type MySQLDumpStats = {
|
|
25
19
|
bytes: number;
|
package/lib/utils/mysql.js
CHANGED
|
@@ -158,15 +158,12 @@ export class MySQLDump {
|
|
|
158
158
|
error,
|
|
159
159
|
stats,
|
|
160
160
|
});
|
|
161
|
-
await this.ntfy.send(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
},
|
|
168
|
-
priority: error ? "high" : "default",
|
|
169
|
-
tags: [error ? "red_circle" : "green_circle"],
|
|
170
|
-
});
|
|
161
|
+
await this.ntfy.send("SQL dump", {
|
|
162
|
+
Package: item.name,
|
|
163
|
+
Size: formatBytes(stats.bytes),
|
|
164
|
+
Files: stats.files,
|
|
165
|
+
Duration: duration(Date.now() - now),
|
|
166
|
+
Error: error?.message,
|
|
167
|
+
}, error);
|
|
171
168
|
}
|
|
172
169
|
}
|
package/lib/utils/ntfy.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Agent } from "undici";
|
|
2
|
+
interface MessageObject {
|
|
3
|
+
[key: string]: string | number | undefined | ({
|
|
4
|
+
key: string;
|
|
5
|
+
value: string | number;
|
|
6
|
+
level?: number;
|
|
7
|
+
} | false)[];
|
|
8
|
+
}
|
|
2
9
|
export declare class Ntfy {
|
|
3
10
|
readonly options: {
|
|
4
11
|
token?: string;
|
|
@@ -11,8 +18,9 @@ export declare class Ntfy {
|
|
|
11
18
|
titlePrefix?: string;
|
|
12
19
|
delay?: number;
|
|
13
20
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
private formatTitle;
|
|
22
|
+
private formatMessage;
|
|
23
|
+
private formatMessageObject;
|
|
24
|
+
send(inTitle: string, message: MessageObject, error?: Error | boolean): Promise<void>;
|
|
18
25
|
}
|
|
26
|
+
export {};
|
package/lib/utils/ntfy.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { unstyle } from "./string.js";
|
|
1
2
|
import { setTimeout } from "timers/promises";
|
|
2
3
|
import { Agent, fetch } from "undici";
|
|
4
|
+
import { styleText } from "util";
|
|
3
5
|
export class Ntfy {
|
|
4
6
|
options;
|
|
5
7
|
agent;
|
|
@@ -11,32 +13,50 @@ export class Ntfy {
|
|
|
11
13
|
connections: 1,
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
formatTitle(title) {
|
|
17
|
+
const text = styleText("cyan", title);
|
|
18
|
+
const prefix = this.options.titlePrefix;
|
|
19
|
+
return prefix ? `[${styleText("magenta", prefix)}] ${text}` : text;
|
|
20
|
+
}
|
|
21
|
+
formatMessage(name, value, level = 0) {
|
|
22
|
+
const pad = " ".repeat(level);
|
|
23
|
+
return `${pad}- ${name}: ${styleText("gray", value.toString())}`;
|
|
24
|
+
}
|
|
25
|
+
formatMessageObject(object, level = 0) {
|
|
26
|
+
return Object.entries(object)
|
|
27
|
+
.filter(([, value]) => value !== undefined)
|
|
28
|
+
.map(([name, value]) => {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return value
|
|
31
|
+
.filter((item) => item !== false)
|
|
32
|
+
.map((item) => this.formatMessage(item.key, item.value.toString(), item.level))
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return this.formatMessage(name, value.toString(), level);
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.join("\n");
|
|
40
|
+
}
|
|
41
|
+
async send(inTitle, message, error) {
|
|
42
|
+
const title = this.formatTitle(inTitle);
|
|
43
|
+
const body = this.formatMessageObject(message);
|
|
28
44
|
const lines = [title, body].filter((v) => v.length);
|
|
29
45
|
if (lines.length)
|
|
30
46
|
console.info([...lines, ""].join("\n"));
|
|
47
|
+
const options = {
|
|
48
|
+
priority: error ? "high" : "default",
|
|
49
|
+
tags: [error ? "red_circle" : "green_circle"],
|
|
50
|
+
};
|
|
31
51
|
try {
|
|
32
52
|
if (this.options.token)
|
|
33
53
|
await fetch(`https://ntfy.sh/${this.options.token}`, {
|
|
34
54
|
dispatcher: this.agent,
|
|
35
55
|
method: "POST",
|
|
36
|
-
body,
|
|
56
|
+
body: unstyle(body),
|
|
37
57
|
headers: {
|
|
38
58
|
Markdown: "yes",
|
|
39
|
-
Title: title,
|
|
59
|
+
Title: unstyle(title),
|
|
40
60
|
Priority: options.priority ?? "default",
|
|
41
61
|
...(options.tags && {
|
|
42
62
|
Tags: options.tags?.join(","),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unstyle(str: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datatruck/restic",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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.6"
|
|
38
38
|
},
|
|
39
39
|
"engine": {
|
|
40
40
|
"node": ">=20.0.0"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { Ntfy } from "./ntfy.js";
|
|
2
|
-
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
3
|
-
export type CommonResticBackupTags = {
|
|
4
|
-
id: string;
|
|
5
|
-
shortId: string;
|
|
6
|
-
hostname: string;
|
|
7
|
-
date: string;
|
|
8
|
-
vendor: string;
|
|
9
|
-
version: string;
|
|
10
|
-
};
|
|
11
|
-
export type ResticBackupTags = CommonResticBackupTags & {
|
|
12
|
-
package: string;
|
|
13
|
-
tags?: string[];
|
|
14
|
-
};
|
|
15
|
-
export type ResticBackupPackage = {
|
|
16
|
-
name: string;
|
|
17
|
-
tags?: string[];
|
|
18
|
-
path: string;
|
|
19
|
-
exclude?: string[];
|
|
20
|
-
};
|
|
21
|
-
export type ResticBackupStats = {
|
|
22
|
-
files: number;
|
|
23
|
-
bytes: number;
|
|
24
|
-
diffBytes: number | undefined;
|
|
25
|
-
};
|
|
26
|
-
export type ResticOptions = {
|
|
27
|
-
name: string;
|
|
28
|
-
tags: CommonResticBackupTags;
|
|
29
|
-
minFreeSpace?: string;
|
|
30
|
-
connection: {
|
|
31
|
-
password: string;
|
|
32
|
-
uri: string;
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
export declare class ResticBackup {
|
|
36
|
-
readonly options: ResticOptions;
|
|
37
|
-
protected ntfy: Ntfy;
|
|
38
|
-
protected log: boolean | undefined;
|
|
39
|
-
readonly processes: {
|
|
40
|
-
name: string;
|
|
41
|
-
error?: Error;
|
|
42
|
-
stats: ResticBackupStats;
|
|
43
|
-
}[];
|
|
44
|
-
protected startTime: number;
|
|
45
|
-
readonly restic: Restic;
|
|
46
|
-
constructor(options: ResticOptions, ntfy: Ntfy, log: boolean | undefined);
|
|
47
|
-
run(input: ResticBackupPackage | ResticBackupPackage[]): Promise<void>;
|
|
48
|
-
protected runSingle(item: ResticBackupPackage): Promise<void>;
|
|
49
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { checkDiskSpace } from "./fs.js";
|
|
2
|
-
import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
|
|
3
|
-
import { formatBytes } from "@datatruck/cli/utils/bytes.js";
|
|
4
|
-
import { duration } from "@datatruck/cli/utils/date.js";
|
|
5
|
-
import { isLocalDir } from "@datatruck/cli/utils/fs.js";
|
|
6
|
-
import { Restic } from "@datatruck/cli/utils/restic.js";
|
|
7
|
-
export class ResticBackup {
|
|
8
|
-
options;
|
|
9
|
-
ntfy;
|
|
10
|
-
log;
|
|
11
|
-
processes = [];
|
|
12
|
-
startTime;
|
|
13
|
-
restic;
|
|
14
|
-
constructor(options, ntfy, log) {
|
|
15
|
-
this.options = options;
|
|
16
|
-
this.ntfy = ntfy;
|
|
17
|
-
this.log = log;
|
|
18
|
-
this.startTime = Date.now();
|
|
19
|
-
this.restic = new Restic({
|
|
20
|
-
env: {
|
|
21
|
-
RESTIC_PASSWORD: options.connection.password,
|
|
22
|
-
RESTIC_REPOSITORY: options.connection.uri,
|
|
23
|
-
},
|
|
24
|
-
log,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
async run(input) {
|
|
28
|
-
const items = Array.isArray(input) ? input : [input];
|
|
29
|
-
for (const item of items) {
|
|
30
|
-
await this.runSingle(item);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async runSingle(item) {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
let error;
|
|
36
|
-
const stats = {
|
|
37
|
-
bytes: 0,
|
|
38
|
-
files: 0,
|
|
39
|
-
diffBytes: undefined,
|
|
40
|
-
};
|
|
41
|
-
try {
|
|
42
|
-
if (isLocalDir(this.options.connection.uri) &&
|
|
43
|
-
!(await this.restic.checkRepository()))
|
|
44
|
-
await this.restic.exec(["init"]);
|
|
45
|
-
const targetPath = isLocalDir(this.options.connection.uri)
|
|
46
|
-
? this.options.connection.uri
|
|
47
|
-
: undefined;
|
|
48
|
-
stats.diffBytes = await checkDiskSpace({
|
|
49
|
-
minFreeSpace: this.options.minFreeSpace,
|
|
50
|
-
minFreeSpacePath: targetPath ?? process.cwd(),
|
|
51
|
-
targetPath,
|
|
52
|
-
rutine: () => {
|
|
53
|
-
const tags = {
|
|
54
|
-
...this.options.tags,
|
|
55
|
-
package: item.name,
|
|
56
|
-
tags: item.tags,
|
|
57
|
-
};
|
|
58
|
-
return this.restic.backup({
|
|
59
|
-
tags: ResticRepository.createSnapshotTags(tags),
|
|
60
|
-
paths: [item.path],
|
|
61
|
-
exclude: item.exclude,
|
|
62
|
-
onStream(data) {
|
|
63
|
-
if (data.message_type === "summary") {
|
|
64
|
-
stats.files = data.total_files_processed;
|
|
65
|
-
stats.bytes = data.total_bytes_processed;
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
catch (inError) {
|
|
73
|
-
error = inError;
|
|
74
|
-
}
|
|
75
|
-
this.processes.push({ name: item.name, error, stats });
|
|
76
|
-
await this.ntfy.send(`Backup`, {
|
|
77
|
-
"- Repository": this.options.name,
|
|
78
|
-
"- Package": item.name,
|
|
79
|
-
"- Size": formatBytes(stats.bytes),
|
|
80
|
-
...(stats.diffBytes !== undefined && {
|
|
81
|
-
"- Size change": (stats.diffBytes > 0 ? "+" : "") + formatBytes(stats.diffBytes),
|
|
82
|
-
}),
|
|
83
|
-
"- Files": stats.files,
|
|
84
|
-
"- Duration": duration(Date.now() - now),
|
|
85
|
-
"- Error": error?.message,
|
|
86
|
-
}, {
|
|
87
|
-
priority: error ? "high" : "default",
|
|
88
|
-
tags: [error ? "red_circle" : "green_circle"],
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|