@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/lib/config.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { MySQLDumpOptions } from "./utils/mysql.js";
2
+ import { Restic } from "@datatruck/cli/utils/restic.js";
1
3
  export type GlobalConfig = {
2
4
  config?: string;
3
5
  verbose?: boolean;
@@ -8,6 +10,7 @@ export type Config = {
8
10
  ntfyToken?: string;
9
11
  minFreeSpace?: string;
10
12
  verbose?: boolean;
13
+ prunePolicy?: PrunePolicy;
11
14
  tasks?: {
12
15
  type: "mysql-dump";
13
16
  packages: string[];
@@ -20,26 +23,66 @@ export type Config = {
20
23
  path: string | false;
21
24
  }[] | string;
22
25
  concurrency?: number;
23
- connection: {
24
- hostname: string;
25
- username: string;
26
- password: string;
27
- database?: string;
28
- };
26
+ connection: MySQLDumpOptions["connection"];
29
27
  };
30
28
  }[];
31
29
  packages: {
32
30
  name: string;
33
31
  path: string;
34
32
  exclude?: string[];
33
+ prunePolicy?: PrunePolicy;
35
34
  }[];
36
35
  repositories: {
37
36
  name: string;
38
37
  password: string;
39
38
  uri: string;
39
+ prunePolicy?: PrunePolicy;
40
40
  }[];
41
41
  };
42
+ export type PrunePolicy = {
43
+ keepMinutely?: number;
44
+ keepDaily?: number;
45
+ keepHourly?: number;
46
+ keepLast?: number;
47
+ keepMonthly?: number;
48
+ keepWeekly?: number;
49
+ keepYearly?: number;
50
+ };
42
51
  export declare function defineConfig(config: Config): Config;
43
52
  export declare function validateConfig(config: unknown): Promise<void>;
44
53
  export declare function readConfigSchemaFile(): Promise<unknown>;
45
54
  export declare function parseConfigFile(path?: string): Promise<Config>;
55
+ export declare class ConfigManager {
56
+ readonly config: Config;
57
+ constructor(config: Config);
58
+ filterRepositories(filter: string[] | undefined): {
59
+ name: string;
60
+ password: string;
61
+ uri: string;
62
+ prunePolicy?: PrunePolicy;
63
+ }[];
64
+ filterPackages(filter: string[] | undefined): {
65
+ name: string;
66
+ path: string;
67
+ exclude?: string[];
68
+ prunePolicy?: PrunePolicy;
69
+ }[];
70
+ findRepository(name: string): {
71
+ name: string;
72
+ password: string;
73
+ uri: string;
74
+ prunePolicy?: PrunePolicy;
75
+ };
76
+ findPackage(name: string): {
77
+ name: string;
78
+ path: string;
79
+ exclude?: string[];
80
+ prunePolicy?: PrunePolicy;
81
+ };
82
+ createRestic(repoName: string, verbose: boolean | undefined): readonly [Restic, {
83
+ name: string;
84
+ password: string;
85
+ uri: string;
86
+ prunePolicy?: PrunePolicy;
87
+ }];
88
+ }
package/lib/config.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { parseJSONFile } from "./utils/fs.js";
2
+ import { Restic } from "@datatruck/cli/utils/restic.js";
3
+ import { match } from "@datatruck/cli/utils/string.js";
2
4
  import { Ajv } from "ajv";
3
5
  export function defineConfig(config) {
4
6
  return config;
@@ -20,3 +22,48 @@ export async function parseConfigFile(path = "datatruck.restic.json") {
20
22
  await validateConfig(config);
21
23
  return config;
22
24
  }
25
+ export class ConfigManager {
26
+ config;
27
+ constructor(config) {
28
+ this.config = config;
29
+ }
30
+ filterRepositories(filter) {
31
+ const repositories = this.config.repositories.filter((v) => filter ? match(v.name, filter) : true);
32
+ if (!repositories.length)
33
+ throw new Error(filter
34
+ ? `No repositories found for filter: ${filter.join(", ")}`
35
+ : `No repositories found`);
36
+ return repositories;
37
+ }
38
+ filterPackages(filter) {
39
+ const packages = this.config.packages.filter((v) => filter ? match(v.name, filter) : true);
40
+ if (!packages.length)
41
+ throw new Error(filter
42
+ ? `No packages found for filter: ${filter.join(", ")}`
43
+ : `No packages found`);
44
+ return packages;
45
+ }
46
+ findRepository(name) {
47
+ const repo = this.config.repositories.find((repo) => repo.name === name);
48
+ if (!repo)
49
+ throw new Error(`Repository '${name}' not found`);
50
+ return repo;
51
+ }
52
+ findPackage(name) {
53
+ const pkg = this.config.packages.find((pkg) => pkg.name === name);
54
+ if (!pkg)
55
+ throw new Error(`Package '${name}' not found`);
56
+ return pkg;
57
+ }
58
+ createRestic(repoName, verbose) {
59
+ const repo = this.findRepository(repoName);
60
+ const restic = new Restic({
61
+ log: verbose,
62
+ env: {
63
+ RESTIC_REPOSITORY: repo.uri,
64
+ RESTIC_PASSWORD: repo.password,
65
+ },
66
+ });
67
+ return [restic, repo];
68
+ }
69
+ }
@@ -0,0 +1,3 @@
1
+ import { Config } from "./config.js";
2
+ import { Command } from "commander";
3
+ export declare function createBin(inConfig?: Config): Command;
@@ -0,0 +1,83 @@
1
+ import { Backup } from "./actions/backup.js";
2
+ import { Copy } from "./actions/copy.js";
3
+ import { Create } from "./actions/create.js";
4
+ import { Init } from "./actions/init.js";
5
+ import { Prune } from "./actions/prune.js";
6
+ import { parseConfigFile } from "./config.js";
7
+ import { parseStringList } from "@datatruck/cli/utils/string.js";
8
+ import { Command } from "commander";
9
+ import { resolve } from "path";
10
+ export function createBin(inConfig) {
11
+ async function load() {
12
+ const globalOptions = { ...program.opts() };
13
+ if (globalOptions.config)
14
+ globalOptions.config = resolve(globalOptions.config);
15
+ const config = inConfig ?? (await parseConfigFile(globalOptions.config));
16
+ globalOptions.verbose = process.env.DEBUG
17
+ ? true
18
+ : globalOptions.verbose || config.verbose;
19
+ return { config, globalOptions };
20
+ }
21
+ const program = new Command();
22
+ program.option("-v, --verbose");
23
+ if (!inConfig)
24
+ program.option("-c, --config <path>", "Path to config file", "datatruck.restic.json");
25
+ program
26
+ .command("create")
27
+ .description("Create config file")
28
+ .option("--cwd <path>", "Current working directory to create config file in", ".")
29
+ .option("-c,--config <path>", "Output path for config file", "datatruck.restic.json")
30
+ .option("-f,--force", "Force overwrite if config file already exists")
31
+ .action(async (options) => {
32
+ const create = new Create();
33
+ await create.run(options);
34
+ });
35
+ program
36
+ .command("init")
37
+ .alias("i")
38
+ .description("Run init action")
39
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
40
+ .action(async (options) => {
41
+ const { config, globalOptions } = await load();
42
+ const init = new Init(config, globalOptions);
43
+ await init.run(options);
44
+ });
45
+ program
46
+ .command("backup")
47
+ .alias("b")
48
+ .description("Run backup action")
49
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
50
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
51
+ .option("--prune", "Prune after backup")
52
+ .action(async (options) => {
53
+ const { config, globalOptions } = await load();
54
+ const backup = new Backup(config, globalOptions);
55
+ await backup.run(options);
56
+ });
57
+ program
58
+ .command("copy")
59
+ .alias("c")
60
+ .description("Run copy action")
61
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
62
+ .requiredOption("-s, --source <name>", "Source repository name")
63
+ .requiredOption("-t, --targets <names>", "Target repository names", (v) => parseStringList(v))
64
+ .option("--prune", "Prune after copy")
65
+ .action(async (options) => {
66
+ const { config, globalOptions } = await load();
67
+ const copy = new Copy(config, globalOptions);
68
+ await copy.run(options);
69
+ });
70
+ program
71
+ .command("prune")
72
+ .alias("p")
73
+ .description("Run prune action")
74
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
75
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
76
+ .option("--prune", "Prune after copy")
77
+ .action(async (options) => {
78
+ const { config, globalOptions } = await load();
79
+ const prune = new Prune(config, globalOptions);
80
+ await prune.run(options);
81
+ });
82
+ return program;
83
+ }
package/lib/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export { Backup, type BackupRunOptions } from "./actions/backup.js";
2
- export { Copy, type CopyRunOptions } from "./actions/copy.js";
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 { type Config, type GlobalConfig, parseConfigFile, defineConfig, validateConfig, } from "./config.js";
4
+ export { Prune, type PruneOptions } from "./actions/prune.js";
5
+ export { type Config, type GlobalConfig, type PrunePolicy, parseConfigFile, defineConfig, validateConfig, } from "./config.js";
6
+ export { createBin } from "./create-bin.js";
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { Backup } from "./actions/backup.js";
2
2
  export { Copy } from "./actions/copy.js";
3
3
  export { Init } from "./actions/init.js";
4
+ export { Prune } from "./actions/prune.js";
4
5
  export { parseConfigFile, defineConfig, validateConfig, } from "./config.js";
6
+ export { createBin } from "./create-bin.js";
@@ -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
+ }>;
@@ -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.3",
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
- }