@datatruck/restic 0.0.3 → 0.0.5

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.
@@ -5,16 +5,11 @@ export type BackupOptions = {
5
5
  prune?: boolean;
6
6
  };
7
7
  export type CommonResticBackupTags = {
8
+ dt: boolean;
8
9
  id: string;
9
- shortId: string;
10
- hostname: string;
11
- date: string;
12
- vendor: string;
13
- version: string;
14
10
  };
15
11
  export type ResticBackupTags = CommonResticBackupTags & {
16
- package: string;
17
- tags?: string[];
12
+ pkg: string;
18
13
  };
19
14
  export declare class Backup extends Action {
20
15
  protected runSingle(repoName: string, pkgName: string, tags: CommonResticBackupTags): Promise<{
@@ -1,17 +1,15 @@
1
1
  import { createRunner, safeRun } from "../utils/async.js";
2
2
  import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
3
3
  import { MySQLDump } from "../utils/mysql.js";
4
+ import { randomString } from "../utils/string.js";
5
+ import { stringifyTags } from "../utils/tags.js";
4
6
  import { Action } from "./base.js";
5
7
  import { Prune } from "./prune.js";
6
- import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
7
- import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
8
8
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
9
9
  import { isLocalDir } from "@datatruck/cli/utils/fs.js";
10
10
  import { progressPercent } from "@datatruck/cli/utils/math.js";
11
11
  import { Restic } from "@datatruck/cli/utils/restic.js";
12
12
  import { match } from "@datatruck/cli/utils/string.js";
13
- import { randomUUID } from "crypto";
14
- import { hostname } from "os";
15
13
  export class Backup extends Action {
16
14
  async runSingle(repoName, pkgName, tags) {
17
15
  const repo = this.cm.findRepository(repoName);
@@ -34,14 +32,19 @@ export class Backup extends Action {
34
32
  minFreeSpace: this.config.minFreeSpace,
35
33
  minFreeSpacePath: targetPath ?? process.cwd(),
36
34
  targetPath,
37
- rutine: () => {
38
- const pkgTags = {
39
- ...tags,
40
- package: pkg.name,
41
- tags: [],
42
- };
43
- return restic.backup({
44
- tags: ResticRepository.createSnapshotTags(pkgTags),
35
+ rutine: async () => {
36
+ const last = await restic.snapshots({
37
+ latest: 1,
38
+ host: this.config.hostname,
39
+ tags: stringifyTags({ pkg: pkg.name }),
40
+ });
41
+ return await restic.backup({
42
+ parent: last.at(0)?.id,
43
+ host: this.config.hostname,
44
+ tags: stringifyTags({
45
+ ...tags,
46
+ pkg: pkg.name,
47
+ }),
45
48
  paths: [pkg.path],
46
49
  exclude: pkg.exclude,
47
50
  onStream(data) {
@@ -55,9 +58,7 @@ export class Backup extends Action {
55
58
  });
56
59
  const snapshots = await restic.snapshots({
57
60
  json: true,
58
- tags: [
59
- ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
60
- ],
61
+ tags: stringifyTags({ pkg: pkg.name }),
61
62
  });
62
63
  snapshotsAmount = snapshots.length;
63
64
  }).start(async (data) => {
@@ -101,14 +102,8 @@ export class Backup extends Action {
101
102
  .filter((repo) => isLocalDir(repo.uri))
102
103
  .map((repo) => repo.uri);
103
104
  const tags = {
104
- id: randomUUID().replaceAll("-", ""),
105
- get shortId() {
106
- return this.id.slice(0, 8);
107
- },
108
- hostname: this.config.hostname ?? hostname(),
109
- date: new Date().toISOString(),
110
- vendor: "dtt-restic",
111
- version: "1",
105
+ dt: true,
106
+ id: randomString(8),
112
107
  };
113
108
  for (const task of tasks ?? []) {
114
109
  if (task.type === "mysql-dump") {
@@ -181,7 +176,7 @@ export class Backup extends Action {
181
176
  !!diskStats.result?.length && { key: "Disk stats", value: "" },
182
177
  ...(diskStats.result?.map((p) => ({
183
178
  key: p.name,
184
- value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
179
+ value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.total - p.free)}%)`,
185
180
  level: 1,
186
181
  })) || []),
187
182
  !!sqlDumpProcesses.length && { key: "SQL Dumps", value: "" },
@@ -8,7 +8,6 @@ export type CopyOptions = {
8
8
  export declare class Copy extends Action {
9
9
  protected initializedRepos: Set<string>;
10
10
  private findSnapshots;
11
- private findPackageTag;
12
11
  runSingle(options: {
13
12
  source: string;
14
13
  target: string;
@@ -1,9 +1,8 @@
1
1
  import { createRunner } from "../utils/async.js";
2
2
  import { checkDiskSpace } from "../utils/fs.js";
3
+ import { parseTags, stringifyTags } from "../utils/tags.js";
3
4
  import { Action } from "./base.js";
4
5
  import { Prune } from "./prune.js";
5
- import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
6
- import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
7
6
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
8
7
  import { isLocalDir } from "@datatruck/cli/utils/fs.js";
9
8
  import { Restic } from "@datatruck/cli/utils/restic.js";
@@ -22,23 +21,16 @@ export class Copy extends Action {
22
21
  latest: 1,
23
22
  group: ["path"],
24
23
  tags: packages
25
- ? packages.map((name) => ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, name))
24
+ ? packages.flatMap((name) => stringifyTags({ pkg: name }))
26
25
  : undefined,
27
26
  }));
28
27
  return snapshots.flatMap((s) => s.snapshots);
29
28
  }
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
29
  async runSingle(options) {
37
30
  const { snapshot } = options;
38
31
  const targetRepo = this.cm.findRepository(options.target);
39
32
  const sourceRepo = this.cm.findRepository(options.source);
40
- const pkgTag = this.findPackageTag(snapshot.tags);
41
- const packageName = pkgTag?.value;
33
+ const packageName = parseTags(snapshot.tags).pkg;
42
34
  const targetPath = isLocalDir(targetRepo.uri) ? targetRepo.uri : undefined;
43
35
  const target = new Restic({
44
36
  log: this.verbose,
@@ -1,8 +1,7 @@
1
1
  import { createRunner, safeRun } from "../utils/async.js";
2
2
  import { checkDiskSpace, fetchMultipleDiskStats } from "../utils/fs.js";
3
+ import { parseTags, stringifyTags } from "../utils/tags.js";
3
4
  import { Action } from "./base.js";
4
- import { SnapshotTagEnum } from "@datatruck/cli/repositories/RepositoryAbstract.js";
5
- import { ResticRepository } from "@datatruck/cli/repositories/ResticRepository.js";
6
5
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
7
6
  import { isLocalDir } from "@datatruck/cli/utils/fs.js";
8
7
  import { progressPercent } from "@datatruck/cli/utils/math.js";
@@ -24,9 +23,7 @@ export class Prune extends Action {
24
23
  json: true,
25
24
  prune: true,
26
25
  args: ["--group-by", ""],
27
- tag: [
28
- ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkgName),
29
- ],
26
+ tag: stringifyTags({ pkg: pkgName }),
30
27
  });
31
28
  stats = {
32
29
  keep: results.at(0)?.keep?.length ?? 0,
@@ -61,10 +58,7 @@ export class Prune extends Action {
61
58
  if (!(await restic.checkRepository()))
62
59
  return [];
63
60
  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
- }));
61
+ const packages = new Set(snapshots.map((s) => parseTags(s.tags ?? []).pkg));
68
62
  return [...packages].filter((v) => typeof v === "string");
69
63
  }
70
64
  async run(options) {
@@ -113,7 +107,7 @@ export class Prune extends Action {
113
107
  !!diskStats.result?.length && { key: "Disk stats", value: "" },
114
108
  ...(diskStats.result?.map((p) => ({
115
109
  key: p.name,
116
- value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
110
+ value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.total - p.free)}%)`,
117
111
  level: 1,
118
112
  })) || []),
119
113
  ],
@@ -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,16 @@
1
+ import { Action } from "./base.js";
2
+ import { logExec } from "@datatruck/cli/utils/cli.js";
3
+ import { spawnSync } from "child_process";
4
+ export class Run extends Action {
5
+ async run(options) {
6
+ const [restic] = this.cm.createRestic(options.repository, this.verbose);
7
+ if (this.verbose)
8
+ logExec("restic", options.args, restic.options.env);
9
+ const p = spawnSync("restic", options.args, {
10
+ stdio: "inherit",
11
+ env: { ...process.env, ...restic.options.env },
12
+ });
13
+ if (p.status)
14
+ process.exit(p.status);
15
+ }
16
+ }
package/lib/bin.d.ts CHANGED
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export {};
package/lib/bin.js CHANGED
@@ -1,2 +1,3 @@
1
+ #!/usr/bin/env node
1
2
  import { createBin } from "./create-bin.js";
2
3
  createBin().parse();
package/lib/create-bin.js CHANGED
@@ -3,6 +3,7 @@ import { Copy } from "./actions/copy.js";
3
3
  import { Create } from "./actions/create.js";
4
4
  import { Init } from "./actions/init.js";
5
5
  import { Prune } from "./actions/prune.js";
6
+ import { Run } from "./actions/run.js";
6
7
  import { parseConfigFile } from "./config.js";
7
8
  import { parseStringList } from "@datatruck/cli/utils/string.js";
8
9
  import { Command } from "commander";
@@ -32,6 +33,21 @@ export function createBin(inConfig) {
32
33
  const create = new Create();
33
34
  await create.run(options);
34
35
  });
36
+ program
37
+ .command("run")
38
+ .description("Run arbitrary restic command")
39
+ .option("-r, --repository <name>", "Repository name")
40
+ .argument("[args...]", "Restic arguments")
41
+ .allowUnknownOption()
42
+ .allowExcessArguments()
43
+ .action(async (args, options) => {
44
+ const { config, globalOptions } = await load();
45
+ if (!options.repository && config.repositories.length !== 1)
46
+ throw new Error("Repository name is required");
47
+ const repository = options.repository ?? config.repositories[0].name;
48
+ const run = new Run(config, globalOptions);
49
+ await run.run({ repository, args });
50
+ });
35
51
  program
36
52
  .command("init")
37
53
  .alias("i")
@@ -79,5 +95,23 @@ export function createBin(inConfig) {
79
95
  const prune = new Prune(config, globalOptions);
80
96
  await prune.run(options);
81
97
  });
98
+ const parse = program.parse.bind(program);
99
+ program.parse = function (args, options) {
100
+ if (!args)
101
+ args = process.argv;
102
+ const [node, script, command, ...rest] = args;
103
+ if (command === "run") {
104
+ const repositoryIndex = rest.findIndex((v) => v === "-r" || v === "--repository");
105
+ if (repositoryIndex !== -1) {
106
+ const repositoryName = rest[repositoryIndex + 1];
107
+ const others = rest.filter((_, i) => i !== repositoryIndex && i !== repositoryIndex + 1);
108
+ args = [node, script, command, "-r", repositoryName, "--", ...others];
109
+ }
110
+ else {
111
+ args = [node, script, command, "--", ...rest];
112
+ }
113
+ }
114
+ return parse(args, options);
115
+ };
82
116
  return program;
83
117
  }
@@ -1 +1,2 @@
1
1
  export declare function unstyle(str: string): string;
2
+ export declare function randomString(length: number): string;
@@ -1,3 +1,13 @@
1
+ import { randomInt } from "crypto";
1
2
  export function unstyle(str) {
2
3
  return str.replace(/\x1B\[[0-9;]*m/g, "");
3
4
  }
5
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
6
+ export function randomString(length) {
7
+ let string = "";
8
+ for (let i = 0; i < length; i++) {
9
+ const index = randomInt(0, charset.length);
10
+ string += charset[index];
11
+ }
12
+ return string;
13
+ }
@@ -0,0 +1,3 @@
1
+ export declare function stringifyTags(inTags: Record<string, any>): string[];
2
+ export declare function parseTagPrefix(tag: string): string | undefined;
3
+ export declare function parseTags(inTags: string[]): Record<string, any>;
@@ -0,0 +1,37 @@
1
+ const prefix = "dt";
2
+ const prefixSep = "-";
3
+ const valueSep = ":";
4
+ const fullPrefix = `${prefix}${prefixSep}`;
5
+ export function stringifyTags(inTags) {
6
+ const tags = [];
7
+ for (const [key, value] of Object.entries(inTags)) {
8
+ const tagName = key === prefix ? key : `${prefix}${prefixSep}${key}`;
9
+ tags.push(value === true ? tagName : `${tagName}${valueSep}${value}`);
10
+ }
11
+ return tags;
12
+ }
13
+ export function parseTagPrefix(tag) {
14
+ if (tag === prefix) {
15
+ return tag;
16
+ }
17
+ else if (tag.startsWith(fullPrefix)) {
18
+ return tag.slice(fullPrefix.length);
19
+ }
20
+ }
21
+ export function parseTags(inTags) {
22
+ const tags = {};
23
+ for (const tag of inTags) {
24
+ const str = parseTagPrefix(tag);
25
+ if (!str) {
26
+ continue;
27
+ }
28
+ else if (str.includes(valueSep)) {
29
+ const [tagName, tagValue] = str.split(valueSep);
30
+ tags[tagName] = tagValue ?? "";
31
+ }
32
+ else {
33
+ tags[str] = true;
34
+ }
35
+ }
36
+ return tags;
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datatruck/restic",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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.6"
37
+ "@datatruck/cli": "0.41.7"
38
38
  },
39
39
  "engine": {
40
40
  "node": ">=20.0.0"