@datatruck/restic 0.0.2 → 0.0.4

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.
@@ -3,6 +3,7 @@ import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
3
3
  import { MySQLDump } from "../utils/mysql.js";
4
4
  import { Action } from "./base.js";
5
5
  import { Prune } from "./prune.js";
6
+ import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
6
7
  import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
7
8
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
8
9
  import { isLocalDir } from "@datatruck/cli/utils/fs.js";
@@ -23,6 +24,7 @@ export class Backup extends Action {
23
24
  },
24
25
  });
25
26
  let space;
27
+ let snapshotsAmount;
26
28
  let bytes = 0;
27
29
  let files = 0;
28
30
  return await createRunner(async () => {
@@ -51,6 +53,13 @@ export class Backup extends Action {
51
53
  });
52
54
  },
53
55
  });
56
+ const snapshots = await restic.snapshots({
57
+ json: true,
58
+ tags: [
59
+ ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
60
+ ],
61
+ });
62
+ snapshotsAmount = snapshots.length;
54
63
  }).start(async (data) => {
55
64
  await this.ntfy.send("Backup", {
56
65
  Repository: repo.name,
@@ -58,6 +67,7 @@ export class Backup extends Action {
58
67
  Size: formatBytes(bytes) +
59
68
  (space !== undefined ? ` (${formatBytes(space.diff, true)})` : ""),
60
69
  Files: files,
70
+ Snapshots: snapshotsAmount,
61
71
  Duration: data.duration,
62
72
  Error: data.error?.message,
63
73
  }, data.error);
@@ -0,0 +1,10 @@
1
+ export type CreateOptions = {
2
+ cwd: string;
3
+ config: string;
4
+ force?: boolean;
5
+ };
6
+ export declare class Create {
7
+ constructor();
8
+ protected random(): string;
9
+ run(options: CreateOptions): Promise<void>;
10
+ }
@@ -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
+ }
@@ -5,6 +5,10 @@ export type PruneOptions = {
5
5
  repositories?: string[];
6
6
  };
7
7
  export declare class Prune extends Action {
8
- protected runSingle(repoName: string, pkgName: string, policy: PrunePolicy): Promise<number>;
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[]>;
9
13
  run(options: PruneOptions): Promise<void>;
10
14
  }
@@ -6,21 +6,14 @@ import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.j
6
6
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
7
7
  import { isLocalDir } from "@datatruck/cli/utils/fs.js";
8
8
  import { progressPercent } from "@datatruck/cli/utils/math.js";
9
- import { Restic } from "@datatruck/cli/utils/restic.js";
9
+ import { match } from "@datatruck/cli/utils/string.js";
10
10
  export class Prune extends Action {
11
11
  async runSingle(repoName, pkgName, policy) {
12
12
  let stats;
13
13
  let space;
14
+ let removed;
14
15
  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
- });
16
+ const [restic, repo] = this.cm.createRestic(repoName, this.verbose);
24
17
  const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
25
18
  space = await checkDiskSpace({
26
19
  minFreeSpace: this.config.minFreeSpace,
@@ -32,7 +25,7 @@ export class Prune extends Action {
32
25
  prune: true,
33
26
  args: ["--group-by", ""],
34
27
  tag: [
35
- ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
28
+ ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkgName),
36
29
  ],
37
30
  });
38
31
  stats = {
@@ -42,11 +35,13 @@ export class Prune extends Action {
42
35
  },
43
36
  });
44
37
  }).start(async (data) => {
38
+ if (stats)
39
+ removed = stats.remove;
45
40
  await this.ntfy.send(`Prune`, {
46
41
  Repository: repoName,
47
42
  Package: pkgName,
48
43
  ...(stats && {
49
- Snapshots: `${stats.keep}`,
44
+ Keeped: `${stats.keep}`,
50
45
  Removed: `${stats.remove}`,
51
46
  }),
52
47
  ...(space !== undefined && {
@@ -56,37 +51,60 @@ export class Prune extends Action {
56
51
  Error: data.error?.message,
57
52
  }, data.error);
58
53
  });
59
- return space?.diff ?? 0;
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");
60
69
  }
61
70
  async run(options) {
62
71
  let globalDiffSize;
72
+ let globalRemoved = 0;
63
73
  let localRepositoryPaths = [];
64
74
  await createRunner(async () => {
65
75
  const repositories = this.cm.filterRepositories(options.repositories);
66
- const packages = this.cm.filterPackages(options.packages);
67
76
  await this.ntfy.send(`Prune start`, {
68
77
  Repositories: repositories.length,
69
- Packages: packages.length,
70
78
  });
71
79
  localRepositoryPaths = repositories
72
80
  .filter((repo) => isLocalDir(repo.uri))
73
81
  .map((repo) => repo.uri);
82
+ let some = false;
74
83
  for (const repo of repositories) {
75
- for (const pkg of packages) {
76
- const policy = pkg.prunePolicy ?? repo.prunePolicy ?? this.config.prunePolicy;
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;
77
89
  if (policy) {
78
- const diffSize = await this.runSingle(repo.name, pkg.name, policy);
90
+ const { diffSize, removed } = await this.runSingle(repo.name, pkgName, policy);
91
+ some = true;
79
92
  if (diffSize !== undefined)
80
93
  globalDiffSize = (globalDiffSize ?? 0) + diffSize;
94
+ if (removed !== undefined)
95
+ globalRemoved += removed;
81
96
  }
82
97
  }
83
98
  }
99
+ if (options.packages && !some)
100
+ throw new Error(`No packages found for filter: ${options.packages.join(", ")}`);
84
101
  }).start(async (data) => {
85
102
  const diskStats = await safeRun(() => fetchMultipleDiskStats(localRepositoryPaths));
86
103
  if (diskStats.error)
87
104
  console.error(diskStats.error);
88
105
  await this.ntfy.send(`Prune end`, {
89
106
  Duration: data.duration,
107
+ Removed: globalRemoved,
90
108
  ...(globalDiffSize !== undefined && {
91
109
  "Diff size": (globalDiffSize > 0 ? "+" : "") + formatBytes(globalDiffSize),
92
110
  }),
@@ -0,0 +1,8 @@
1
+ import { Action } from "./base.js";
2
+ export type RunOptions = {
3
+ repository: string;
4
+ args: string[];
5
+ };
6
+ export declare class Run extends Action {
7
+ run(options: RunOptions): Promise<void>;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { Action } from "./base.js";
2
+ import { spawnSync } from "child_process";
3
+ export class Run extends Action {
4
+ async run(options) {
5
+ const [restic] = this.cm.createRestic(options.repository, this.verbose);
6
+ const p = restic["createProcess"](options.args, { $log: true });
7
+ const exit = spawnSync(p["command"], p["argv"]?.map((v) => v.toString()), { stdio: "inherit", env: p["options"]?.env });
8
+ if (exit.status)
9
+ process.exit(exit.status);
10
+ }
11
+ }
package/lib/bin.js CHANGED
@@ -1,72 +1,2 @@
1
- import { Backup } from "./actions/backup.js";
2
- import { Copy } from "./actions/copy.js";
3
- import { Init } from "./actions/init.js";
4
- import { Prune } from "./actions/prune.js";
5
- import { parseConfigFile } from "./config.js";
6
- import { parseStringList } from "@datatruck/cli/utils/string.js";
7
- import { program } from "commander";
8
- import { resolve } from "path";
9
- function getGlobalOptions() {
10
- const options = program.opts();
11
- return {
12
- ...options,
13
- config: resolve(options.config),
14
- verbose: process.env.DEBUG ? true : options.verbose,
15
- };
16
- }
17
- async function load() {
18
- const globalOptions = getGlobalOptions();
19
- const config = await parseConfigFile(globalOptions.config);
20
- return { globalOptions, config };
21
- }
22
- program
23
- .option("-v, --verbose")
24
- .option("-c, --config <path>", "Path to config file", "datatruck.restic.json");
25
- program
26
- .command("init")
27
- .alias("i")
28
- .description("Run init action")
29
- .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
30
- .action(async (options) => {
31
- const { config, globalOptions } = await load();
32
- const init = new Init(config, globalOptions);
33
- await init.run(options);
34
- });
35
- program
36
- .command("backup")
37
- .alias("b")
38
- .description("Run backup action")
39
- .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
40
- .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
41
- .option("--prune", "Prune after backup")
42
- .action(async (options) => {
43
- const { config, globalOptions } = await load();
44
- const backup = new Backup(config, globalOptions);
45
- await backup.run(options);
46
- });
47
- program
48
- .command("copy")
49
- .alias("c")
50
- .description("Run copy action")
51
- .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
52
- .requiredOption("-s, --source <name>", "Source repository name")
53
- .requiredOption("-t, --targets <names>", "Target repository names", (v) => parseStringList(v))
54
- .option("--prune", "Prune after copy")
55
- .action(async (options) => {
56
- const { config, globalOptions } = await load();
57
- const copy = new Copy(config, globalOptions);
58
- await copy.run(options);
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
- });
72
- program.parse();
1
+ import { createBin } from "./create-bin.js";
2
+ createBin().parse();
package/lib/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { MySQLDumpOptions } from "./utils/mysql.js";
2
+ import { Restic } from "@datatruck/cli/utils/restic.js";
2
3
  export type GlobalConfig = {
3
4
  config?: string;
4
5
  verbose?: boolean;
@@ -78,4 +79,10 @@ export declare class ConfigManager {
78
79
  exclude?: string[];
79
80
  prunePolicy?: PrunePolicy;
80
81
  };
82
+ createRestic(repoName: string, verbose: boolean | undefined): readonly [Restic, {
83
+ name: string;
84
+ password: string;
85
+ uri: string;
86
+ prunePolicy?: PrunePolicy;
87
+ }];
81
88
  }
package/lib/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseJSONFile } from "./utils/fs.js";
2
+ import { Restic } from "@datatruck/cli/utils/restic.js";
2
3
  import { match } from "@datatruck/cli/utils/string.js";
3
4
  import { Ajv } from "ajv";
4
5
  export function defineConfig(config) {
@@ -29,13 +30,17 @@ export class ConfigManager {
29
30
  filterRepositories(filter) {
30
31
  const repositories = this.config.repositories.filter((v) => filter ? match(v.name, filter) : true);
31
32
  if (!repositories.length)
32
- throw new Error(`No repositories found for filter: ${filter?.join(", ")}`);
33
+ throw new Error(filter
34
+ ? `No repositories found for filter: ${filter.join(", ")}`
35
+ : `No repositories found`);
33
36
  return repositories;
34
37
  }
35
38
  filterPackages(filter) {
36
39
  const packages = this.config.packages.filter((v) => filter ? match(v.name, filter) : true);
37
40
  if (!packages.length)
38
- throw new Error(`No packages found for filter: ${filter?.join(", ")}`);
41
+ throw new Error(filter
42
+ ? `No packages found for filter: ${filter.join(", ")}`
43
+ : `No packages found`);
39
44
  return packages;
40
45
  }
41
46
  findRepository(name) {
@@ -50,4 +55,15 @@ export class ConfigManager {
50
55
  throw new Error(`Package '${name}' not found`);
51
56
  return pkg;
52
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
+ }
53
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,112 @@
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 { Run } from "./actions/run.js";
7
+ import { parseConfigFile } from "./config.js";
8
+ import { parseStringList } from "@datatruck/cli/utils/string.js";
9
+ import { Command } from "commander";
10
+ import { resolve } from "path";
11
+ export function createBin(inConfig) {
12
+ async function load() {
13
+ const globalOptions = { ...program.opts() };
14
+ if (globalOptions.config)
15
+ globalOptions.config = resolve(globalOptions.config);
16
+ const config = inConfig ?? (await parseConfigFile(globalOptions.config));
17
+ globalOptions.verbose = process.env.DEBUG
18
+ ? true
19
+ : globalOptions.verbose || config.verbose;
20
+ return { config, globalOptions };
21
+ }
22
+ const program = new Command();
23
+ program.option("-v, --verbose");
24
+ if (!inConfig)
25
+ program.option("-c, --config <path>", "Path to config file", "datatruck.restic.json");
26
+ program
27
+ .command("create")
28
+ .description("Create config file")
29
+ .option("--cwd <path>", "Current working directory to create config file in", ".")
30
+ .option("-c,--config <path>", "Output path for config file", "datatruck.restic.json")
31
+ .option("-f,--force", "Force overwrite if config file already exists")
32
+ .action(async (options) => {
33
+ const create = new Create();
34
+ await create.run(options);
35
+ });
36
+ program
37
+ .command("run", {})
38
+ .description("Run arbitrary restic command")
39
+ .argument("<repository>", "Repository name")
40
+ .argument("[args...]", "Restic arguments")
41
+ .allowUnknownOption()
42
+ .allowExcessArguments()
43
+ .action(async (repository, args) => {
44
+ const { config, globalOptions } = await load();
45
+ const run = new Run(config, globalOptions);
46
+ await run.run({ repository, args });
47
+ });
48
+ program
49
+ .command("init")
50
+ .alias("i")
51
+ .description("Run init action")
52
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
53
+ .action(async (options) => {
54
+ const { config, globalOptions } = await load();
55
+ const init = new Init(config, globalOptions);
56
+ await init.run(options);
57
+ });
58
+ program
59
+ .command("backup")
60
+ .alias("b")
61
+ .description("Run backup action")
62
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
63
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
64
+ .option("--prune", "Prune after backup")
65
+ .action(async (options) => {
66
+ const { config, globalOptions } = await load();
67
+ const backup = new Backup(config, globalOptions);
68
+ await backup.run(options);
69
+ });
70
+ program
71
+ .command("copy")
72
+ .alias("c")
73
+ .description("Run copy action")
74
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
75
+ .requiredOption("-s, --source <name>", "Source repository name")
76
+ .requiredOption("-t, --targets <names>", "Target repository names", (v) => parseStringList(v))
77
+ .option("--prune", "Prune after copy")
78
+ .action(async (options) => {
79
+ const { config, globalOptions } = await load();
80
+ const copy = new Copy(config, globalOptions);
81
+ await copy.run(options);
82
+ });
83
+ program
84
+ .command("prune")
85
+ .alias("p")
86
+ .description("Run prune action")
87
+ .option("-r, --repositories <names>", "Repository names", (v) => parseStringList(v))
88
+ .option("-p, --packages <packages>", "Package names", (v) => parseStringList(v))
89
+ .option("--prune", "Prune after copy")
90
+ .action(async (options) => {
91
+ const { config, globalOptions } = await load();
92
+ const prune = new Prune(config, globalOptions);
93
+ await prune.run(options);
94
+ });
95
+ const parse = program.parse.bind(program);
96
+ program.parse = function (args, options) {
97
+ if (!args)
98
+ args = process.argv;
99
+ const [node, script, ...rest] = args;
100
+ const commandIndex = rest.findIndex((v) => !v.startsWith("-"));
101
+ const command = rest[commandIndex];
102
+ if (command === "run") {
103
+ args = [
104
+ node,
105
+ script,
106
+ ...rest.flatMap((value, index) => commandIndex === index ? [value, "--"] : value),
107
+ ];
108
+ }
109
+ return parse(args, options);
110
+ };
111
+ return program;
112
+ }
package/lib/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { Backup, type BackupOptions } from "./actions/backup.js";
2
2
  export { Copy, type CopyOptions } from "./actions/copy.js";
3
3
  export { Init, type InitOptions } from "./actions/init.js";
4
4
  export { Prune, type PruneOptions } from "./actions/prune.js";
5
- export { type Config, type GlobalConfig, parseConfigFile, defineConfig, validateConfig, } from "./config.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
@@ -3,3 +3,4 @@ export { Copy } from "./actions/copy.js";
3
3
  export { Init } from "./actions/init.js";
4
4
  export { Prune } from "./actions/prune.js";
5
5
  export { parseConfigFile, defineConfig, validateConfig, } from "./config.js";
6
+ export { createBin } from "./create-bin.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datatruck/restic",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Tool for creating and managing backups",
5
5
  "homepage": "https://github.com/swordev/datatruck#readme",
6
6
  "bugs": {