@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.
@@ -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<number | undefined>;
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 { readFile } from "fs/promises";
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 next - prev;
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
  }
@@ -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: MySQLDumpConnection;
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;
@@ -158,15 +158,12 @@ export class MySQLDump {
158
158
  error,
159
159
  stats,
160
160
  });
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
- }, {
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
  }
@@ -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
- send(inTitle: string, message: any[] | string | Record<string, any>, options?: {
15
- priority?: "default" | "high";
16
- tags?: string[];
17
- }): Promise<void>;
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
- async send(inTitle, message, options = {}) {
15
- const title = this.options.titlePrefix
16
- ? `${this.options.titlePrefix}${inTitle}`
17
- : inTitle;
18
- const body = Array.isArray(message)
19
- ? message
20
- .filter((v) => typeof v === "string" || typeof v === "number")
21
- .join("\n")
22
- : typeof message === "object" && !!message
23
- ? Object.entries(message)
24
- .filter(([, value]) => value !== undefined)
25
- .map(([name, value]) => `${name}: ${value}`)
26
- .join("\n")
27
- : message;
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;
@@ -0,0 +1,3 @@
1
+ export function unstyle(str) {
2
+ return str.replace(/\x1B\[[0-9;]*m/g, "");
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datatruck/restic",
3
- "version": "0.0.1",
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.5"
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
- }