@datatruck/restic 0.0.4 → 0.0.6

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);
@@ -23,25 +21,35 @@ export class Backup extends Action {
23
21
  RESTIC_REPOSITORY: repo.uri,
24
22
  },
25
23
  });
24
+ let logId;
26
25
  let space;
27
26
  let snapshotsAmount;
28
27
  let bytes = 0;
29
28
  let files = 0;
30
29
  return await createRunner(async () => {
30
+ logId = await this.ntfy.send("Backup", {
31
+ Repository: repo.name,
32
+ Package: pkg.name,
33
+ });
31
34
  await restic.tryInit();
32
35
  const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
33
36
  space = await checkDiskSpace({
34
37
  minFreeSpace: this.config.minFreeSpace,
35
38
  minFreeSpacePath: targetPath ?? process.cwd(),
36
39
  targetPath,
37
- rutine: () => {
38
- const pkgTags = {
39
- ...tags,
40
- package: pkg.name,
41
- tags: [],
42
- };
43
- return restic.backup({
44
- tags: ResticRepository.createSnapshotTags(pkgTags),
40
+ rutine: async () => {
41
+ const last = await restic.snapshots({
42
+ latest: 1,
43
+ host: this.config.hostname,
44
+ tags: stringifyTags({ pkg: pkg.name }),
45
+ });
46
+ return await restic.backup({
47
+ parent: last.at(0)?.id,
48
+ host: this.config.hostname,
49
+ tags: stringifyTags({
50
+ ...tags,
51
+ pkg: pkg.name,
52
+ }),
45
53
  paths: [pkg.path],
46
54
  exclude: pkg.exclude,
47
55
  onStream(data) {
@@ -55,9 +63,7 @@ export class Backup extends Action {
55
63
  });
56
64
  const snapshots = await restic.snapshots({
57
65
  json: true,
58
- tags: [
59
- ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkg.name),
60
- ],
66
+ tags: stringifyTags({ pkg: pkg.name }),
61
67
  });
62
68
  snapshotsAmount = snapshots.length;
63
69
  }).start(async (data) => {
@@ -70,7 +76,7 @@ export class Backup extends Action {
70
76
  Snapshots: snapshotsAmount,
71
77
  Duration: data.duration,
72
78
  Error: data.error?.message,
73
- }, data.error);
79
+ }, { error: data.error, logId });
74
80
  return {
75
81
  error: data.error,
76
82
  files,
@@ -87,12 +93,17 @@ export class Backup extends Action {
87
93
  const sqlDumps = [];
88
94
  const backups = [];
89
95
  let localRepositoryPaths = [];
96
+ const tags = {
97
+ dt: true,
98
+ id: randomString(8),
99
+ };
90
100
  await createRunner(async () => {
91
101
  const repositories = this.cm.filterRepositories(options.repositories);
92
102
  const packages = this.cm.filterPackages(options.packages);
93
103
  const packageNames = packages.map((p) => p.name);
94
104
  const tasks = this.filterTasks(packageNames);
95
105
  await this.ntfy.send(`Backup start`, {
106
+ Id: tags.id,
96
107
  Repositories: repositories.length,
97
108
  Packages: packageNames.length,
98
109
  Tasks: tasks?.length,
@@ -100,16 +111,6 @@ export class Backup extends Action {
100
111
  localRepositoryPaths = repositories
101
112
  .filter((repo) => isLocalDir(repo.uri))
102
113
  .map((repo) => repo.uri);
103
- 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",
112
- };
113
114
  for (const task of tasks ?? []) {
114
115
  if (task.type === "mysql-dump") {
115
116
  const mysqlDump = new MySQLDump({
@@ -144,8 +145,13 @@ export class Backup extends Action {
144
145
  }
145
146
  }
146
147
  }).start(async (data) => {
147
- for (const sqlDump of sqlDumps)
148
- await sqlDump.cleanup();
148
+ try {
149
+ for (const sqlDump of sqlDumps)
150
+ await sqlDump.cleanup();
151
+ }
152
+ catch (error) {
153
+ console.error(error);
154
+ }
149
155
  const summary = backups.reduce((acc, p) => {
150
156
  if (!acc[p.pkgName])
151
157
  acc[p.pkgName] = {
@@ -174,6 +180,7 @@ export class Backup extends Action {
174
180
  if (diskStats.error)
175
181
  console.error(diskStats.error);
176
182
  await this.ntfy.send("Backup end", {
183
+ Id: tags.id,
177
184
  Duration: data.duration,
178
185
  Size: formatBytes(size),
179
186
  Error: data.error?.message,
@@ -181,7 +188,7 @@ export class Backup extends Action {
181
188
  !!diskStats.result?.length && { key: "Disk stats", value: "" },
182
189
  ...(diskStats.result?.map((p) => ({
183
190
  key: p.name,
184
- value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
191
+ value: `${formatBytes(p.occupied)}/${formatBytes(p.total)} (${progressPercent(p.total, p.occupied)}%)`,
185
192
  level: 1,
186
193
  })) || []),
187
194
  !!sqlDumpProcesses.length && { key: "SQL Dumps", value: "" },
@@ -197,7 +204,7 @@ export class Backup extends Action {
197
204
  level: 1,
198
205
  })),
199
206
  ],
200
- }, error);
207
+ }, { error });
201
208
  if (options.prune) {
202
209
  await new Prune(this.config, this.global).run({
203
210
  packages: options.packages,
@@ -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,
@@ -48,8 +40,14 @@ export class Copy extends Action {
48
40
  ["GODEBUG"]: "http2client=0",
49
41
  },
50
42
  });
43
+ let logId;
51
44
  let space;
52
45
  await createRunner(async () => {
46
+ logId = await this.ntfy.send("Copy", {
47
+ Id: snapshot.short_id,
48
+ Source: sourceRepo.name,
49
+ Target: targetRepo.name,
50
+ });
53
51
  if (!this.initializedRepos.has(targetRepo.name)) {
54
52
  await target.tryInit();
55
53
  this.initializedRepos.add(targetRepo.name);
@@ -74,7 +72,7 @@ export class Copy extends Action {
74
72
  }),
75
73
  Duration: data.duration,
76
74
  Error: data.error?.message,
77
- }, data.error);
75
+ }, { error: data.error, logId });
78
76
  });
79
77
  return space?.diff ?? 0;
80
78
  }
@@ -107,7 +105,7 @@ export class Copy extends Action {
107
105
  }),
108
106
  Duration: data.duration,
109
107
  Error: data.error?.message,
110
- }, data.error);
108
+ }, { error: data.error });
111
109
  if (options.prune) {
112
110
  await new Prune(this.config, this.global).run({
113
111
  packages: options.packages,
@@ -20,7 +20,7 @@ export class Init extends Action {
20
20
  Exists: exists ? "yes" : "no",
21
21
  Duration: data.duration,
22
22
  Error: data.error?.message,
23
- }, data.error);
23
+ }, { error: data.error });
24
24
  });
25
25
  }
26
26
  async run(options) {
@@ -1,18 +1,22 @@
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";
9
8
  import { match } from "@datatruck/cli/utils/string.js";
10
9
  export class Prune extends Action {
11
10
  async runSingle(repoName, pkgName, policy) {
11
+ let logId;
12
12
  let stats;
13
13
  let space;
14
14
  let removed;
15
15
  await createRunner(async () => {
16
+ logId = await this.ntfy.send("Prune", {
17
+ Repository: repoName,
18
+ Package: pkgName,
19
+ });
16
20
  const [restic, repo] = this.cm.createRestic(repoName, this.verbose);
17
21
  const targetPath = isLocalDir(repo.uri) ? repo.uri : undefined;
18
22
  space = await checkDiskSpace({
@@ -24,9 +28,7 @@ export class Prune extends Action {
24
28
  json: true,
25
29
  prune: true,
26
30
  args: ["--group-by", ""],
27
- tag: [
28
- ResticRepository.createSnapshotTag(SnapshotTagEnum.PACKAGE, pkgName),
29
- ],
31
+ tag: stringifyTags({ pkg: pkgName }),
30
32
  });
31
33
  stats = {
32
34
  keep: results.at(0)?.keep?.length ?? 0,
@@ -49,7 +51,7 @@ export class Prune extends Action {
49
51
  }),
50
52
  Duration: data.duration,
51
53
  Error: data.error?.message,
52
- }, data.error);
54
+ }, { error: data.error, logId });
53
55
  });
54
56
  return {
55
57
  diffSize: space?.diff ?? 0,
@@ -61,10 +63,7 @@ export class Prune extends Action {
61
63
  if (!(await restic.checkRepository()))
62
64
  return [];
63
65
  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
- }));
66
+ const packages = new Set(snapshots.map((s) => parseTags(s.tags ?? []).pkg));
68
67
  return [...packages].filter((v) => typeof v === "string");
69
68
  }
70
69
  async run(options) {
@@ -113,7 +112,7 @@ export class Prune extends Action {
113
112
  !!diskStats.result?.length && { key: "Disk stats", value: "" },
114
113
  ...(diskStats.result?.map((p) => ({
115
114
  key: p.name,
116
- value: `${formatBytes(p.free)}/${formatBytes(p.total)} (${progressPercent(p.total, p.free)}%)`,
115
+ value: `${formatBytes(p.occupied)}/${formatBytes(p.total)} (${progressPercent(p.total, p.occupied)}%)`,
117
116
  level: 1,
118
117
  })) || []),
119
118
  ],
@@ -1,11 +1,16 @@
1
1
  import { Action } from "./base.js";
2
+ import { logExec } from "@datatruck/cli/utils/cli.js";
2
3
  import { spawnSync } from "child_process";
3
4
  export class Run extends Action {
4
5
  async run(options) {
5
6
  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);
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);
10
15
  }
11
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
@@ -34,14 +34,17 @@ export function createBin(inConfig) {
34
34
  await create.run(options);
35
35
  });
36
36
  program
37
- .command("run", {})
37
+ .command("run")
38
38
  .description("Run arbitrary restic command")
39
- .argument("<repository>", "Repository name")
39
+ .option("-r, --repository <name>", "Repository name")
40
40
  .argument("[args...]", "Restic arguments")
41
41
  .allowUnknownOption()
42
42
  .allowExcessArguments()
43
- .action(async (repository, args) => {
43
+ .action(async (args, options) => {
44
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;
45
48
  const run = new Run(config, globalOptions);
46
49
  await run.run({ repository, args });
47
50
  });
@@ -96,15 +99,17 @@ export function createBin(inConfig) {
96
99
  program.parse = function (args, options) {
97
100
  if (!args)
98
101
  args = process.argv;
99
- const [node, script, ...rest] = args;
100
- const commandIndex = rest.findIndex((v) => !v.startsWith("-"));
101
- const command = rest[commandIndex];
102
+ const [node, script, command, ...rest] = args;
102
103
  if (command === "run") {
103
- args = [
104
- node,
105
- script,
106
- ...rest.flatMap((value, index) => commandIndex === index ? [value, "--"] : value),
107
- ];
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
+ }
108
113
  }
109
114
  return parse(args, options);
110
115
  };
package/lib/utils/fs.d.ts CHANGED
@@ -12,5 +12,6 @@ export declare function getDiskName(inPath: string): Promise<string>;
12
12
  export declare function fetchMultipleDiskStats(paths: string[]): Promise<{
13
13
  name: string;
14
14
  free: number;
15
+ occupied: number;
15
16
  total: number;
16
17
  }[]>;
package/lib/utils/fs.js CHANGED
@@ -54,6 +54,7 @@ export async function fetchMultipleDiskStats(paths) {
54
54
  result.push({
55
55
  name: await getDiskName(path),
56
56
  ...stats,
57
+ occupied: stats.total - stats.free,
57
58
  });
58
59
  }
59
60
  return result;
@@ -54,6 +54,7 @@ import { runParallel } from "@datatruck/cli/utils/async.js";
54
54
  import { formatBytes } from "@datatruck/cli/utils/bytes.js";
55
55
  import { duration } from "@datatruck/cli/utils/date.js";
56
56
  import { ensureFreeDiskSpace, fetchDiskStats, } from "@datatruck/cli/utils/fs.js";
57
+ import { progressPercent } from "@datatruck/cli/utils/math.js";
57
58
  import { createMysqlCli } from "@datatruck/cli/utils/mysql.js";
58
59
  import { match } from "@datatruck/cli/utils/string.js";
59
60
  import { existsSync } from "fs";
@@ -101,7 +102,7 @@ export class MySQLDump {
101
102
  for (const outPath of this.outPaths) {
102
103
  const parentFolder = basename(dirname(outPath));
103
104
  if (!parentFolder.startsWith("sql-dump"))
104
- throw new Error(`sql-dump out dir must begins with 'sql-dump': ${outPath}`);
105
+ throw new Error(`sql-dump out dir base name must begins with 'sql-dump': ${outPath}`);
105
106
  if (existsSync(outPath))
106
107
  await rm(outPath, { recursive: true });
107
108
  if (!init)
@@ -129,7 +130,20 @@ export class MySQLDump {
129
130
  for (const item of items)
130
131
  this.outPaths.add(dirname(item.out));
131
132
  await this.cleanup(true);
133
+ let logId;
134
+ let logIntervalId;
132
135
  try {
136
+ logId = await this.ntfy.send("SQL dump", {
137
+ Name: item.name,
138
+ });
139
+ logIntervalId = setInterval(async () => {
140
+ await this.ntfy.send("SQL dump", {
141
+ Name: item.name,
142
+ Size: formatBytes(stats.bytes),
143
+ Files: `${stats.files}/${items.length} (${progressPercent(items.length, stats.files)}%)`,
144
+ Duration: duration(Date.now() - now),
145
+ }, { logId });
146
+ }, 30_000);
133
147
  await runParallel({
134
148
  items,
135
149
  concurrency: this.options.concurrency ?? 1,
@@ -153,17 +167,20 @@ export class MySQLDump {
153
167
  catch (inError) {
154
168
  error = inError;
155
169
  }
170
+ finally {
171
+ clearInterval(logIntervalId);
172
+ }
156
173
  this.processes.push({
157
174
  name: item.name,
158
175
  error,
159
176
  stats,
160
177
  });
161
178
  await this.ntfy.send("SQL dump", {
162
- Package: item.name,
179
+ Name: item.name,
163
180
  Size: formatBytes(stats.bytes),
164
181
  Files: stats.files,
165
182
  Duration: duration(Date.now() - now),
166
183
  Error: error?.message,
167
- }, error);
184
+ }, { error, logId });
168
185
  }
169
186
  }
@@ -21,6 +21,9 @@ export declare class Ntfy {
21
21
  private formatTitle;
22
22
  private formatMessage;
23
23
  private formatMessageObject;
24
- send(inTitle: string, message: MessageObject, error?: Error | boolean): Promise<void>;
24
+ send(inTitle: string, message: MessageObject, options?: {
25
+ error?: Error | boolean;
26
+ logId?: string;
27
+ }): Promise<string | undefined>;
25
28
  }
26
29
  export {};
package/lib/utils/ntfy.js CHANGED
@@ -38,31 +38,37 @@ export class Ntfy {
38
38
  })
39
39
  .join("\n");
40
40
  }
41
- async send(inTitle, message, error) {
41
+ async send(inTitle, message, options = {}) {
42
42
  const title = this.formatTitle(inTitle);
43
43
  const body = this.formatMessageObject(message);
44
44
  const lines = [title, body].filter((v) => v.length);
45
45
  if (lines.length)
46
46
  console.info([...lines, ""].join("\n"));
47
- const options = {
48
- priority: error ? "high" : "default",
49
- tags: [error ? "red_circle" : "green_circle"],
47
+ const ntfyOptions = {
48
+ priority: options.error ? "high" : "default",
49
+ tags: [options.error ? "red_circle" : "green_circle"],
50
50
  };
51
51
  try {
52
- if (this.options.token)
53
- await fetch(`https://ntfy.sh/${this.options.token}`, {
52
+ if (this.options.token) {
53
+ const token = options.logId
54
+ ? `${this.options.token}/${options.logId}`
55
+ : this.options.token;
56
+ const response = await fetch(`https://ntfy.sh/${token}`, {
54
57
  dispatcher: this.agent,
55
58
  method: "POST",
56
59
  body: unstyle(body),
57
60
  headers: {
58
61
  Markdown: "yes",
59
62
  Title: unstyle(title),
60
- Priority: options.priority ?? "default",
61
- ...(options.tags && {
62
- Tags: options.tags?.join(","),
63
+ Priority: ntfyOptions.priority ?? "default",
64
+ ...(ntfyOptions.tags && {
65
+ Tags: ntfyOptions.tags?.join(","),
63
66
  }),
64
67
  },
65
68
  });
69
+ const json = (await response.json());
70
+ return json.id;
71
+ }
66
72
  await setTimeout(this.options.delay ?? 800);
67
73
  }
68
74
  catch (error) {
@@ -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.4",
3
+ "version": "0.0.6",
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"