@procedures/restic 0.3.0 → 0.4.0
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/CHANGELOG.md +31 -0
- package/README.md +37 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/executable.d.ts +25 -0
- package/dist/executable.d.ts.map +1 -0
- package/dist/executable.js +44 -0
- package/dist/executable.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +13 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +18 -4
- package/dist/module.js.map +1 -1
- package/dist/procedures.d.ts +74 -1
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +59 -25
- package/dist/procedures.js.map +1 -1
- package/package.json +3 -3
- package/src/executable.ts +54 -0
- package/src/index.ts +1 -0
- package/src/module.ts +18 -4
- package/src/procedures.ts +114 -35
package/src/procedures.ts
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* restic reads the repository and its keys from the environment (`RESTIC_REPOSITORY`,
|
|
5
5
|
* `RESTIC_PASSWORD`, `B2_ACCOUNT_ID`, `AWS_ACCESS_KEY_ID` and the others), so no secret is an
|
|
6
6
|
* input. `--password-command` is refused: it starts a program of the caller.
|
|
7
|
+
*
|
|
8
|
+
* A host gives secrets only to the restic process with the `env` option of `runRestic()` or of
|
|
9
|
+
* `createResticProcedures()`. The procedure input has no field for them.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { createProcedure, zodAdapter, type AnyProcedure, type ProcedureContext } from "@procedures/client";
|
|
10
|
-
import { runCommand } from "@procedures/shell/command";
|
|
13
|
+
import { runCommand, type CommandOptions, type CommandResult } from "@procedures/shell/command";
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { buildArgv, parseJsonOutput, type ArgValue } from "./argv.js";
|
|
13
16
|
|
|
@@ -32,20 +35,25 @@ const RunInputSchema = z.object({
|
|
|
32
35
|
});
|
|
33
36
|
type RunInput = z.infer<typeof RunInputSchema>;
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
/** The result of a restic command. */
|
|
39
|
+
export interface ResticResult {
|
|
36
40
|
exitCode: number;
|
|
37
41
|
success: boolean;
|
|
38
42
|
stdout: string;
|
|
39
43
|
stderr: string;
|
|
40
44
|
/** The JSON output, when `json` is true and restic gave JSON */
|
|
41
45
|
json?: unknown;
|
|
46
|
+
/** Present when an output stream was longer than `maxOutputBytes`: which one lost bytes. */
|
|
47
|
+
truncated?: { stdout: boolean; stderr: boolean } | undefined;
|
|
42
48
|
}
|
|
49
|
+
type RunOutput = ResticResult;
|
|
43
50
|
const RunOutputSchema = z.object({
|
|
44
51
|
exitCode: z.number(),
|
|
45
52
|
success: z.boolean(),
|
|
46
53
|
stdout: z.string(),
|
|
47
54
|
stderr: z.string(),
|
|
48
55
|
json: z.unknown().optional(),
|
|
56
|
+
truncated: z.object({ stdout: z.boolean(), stderr: z.boolean() }).optional(),
|
|
49
57
|
});
|
|
50
58
|
|
|
51
59
|
/** A restic command: the subcommand path, its arguments and the repository. */
|
|
@@ -66,37 +74,108 @@ export function resticArgv(input: ResticCommand): string[] {
|
|
|
66
74
|
return buildArgv({ path: input.path, args: input.args, positional: input.positional, flags });
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
/** A function that starts a program: `runCommand` of `@procedures/shell/command`, or a fake in a test. */
|
|
78
|
+
export type ResticRunner = (command: string, options: CommandOptions) => Promise<CommandResult>;
|
|
79
|
+
|
|
80
|
+
/** The options of `runRestic()` that do not come from the command. */
|
|
81
|
+
export interface ResticRunOptions {
|
|
82
|
+
/** The restic program (default: `RESTIC_EXE` of the environment of the host, then `restic`) */
|
|
83
|
+
readonly executable?: string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The variables for the restic process only, for example `RESTIC_PASSWORD`,
|
|
86
|
+
* `RESTIC_REPOSITORY` or `B2_ACCOUNT_ID`. They go on top of the environment of the host. The
|
|
87
|
+
* value `undefined` removes a variable of the host from the environment of restic.
|
|
88
|
+
*/
|
|
89
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
90
|
+
/** The working folder */
|
|
91
|
+
readonly cwd?: string | undefined;
|
|
92
|
+
/** The time limit, in milliseconds. At the limit, the process tree stops. */
|
|
93
|
+
readonly timeout?: number | undefined;
|
|
94
|
+
/** When the signal aborts, the process tree stops. */
|
|
95
|
+
readonly signal?: AbortSignal | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* The most bytes kept of each output stream (default: 64 MiB, the default of `runCommand`).
|
|
98
|
+
* `restic ls` of a large snapshot can give more. Past the limit, the result has `truncated`.
|
|
99
|
+
*/
|
|
100
|
+
readonly maxOutputBytes?: number | undefined;
|
|
101
|
+
/** The function that starts restic (default: `runCommand`). A test gives a fake. */
|
|
102
|
+
readonly run?: ResticRunner | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* This function starts a restic command with argv, not through a shell, and reads its JSON
|
|
107
|
+
* output. `json` is true when the command does not set it, as in `restic.run`. The `env` values
|
|
108
|
+
* go only to the restic process: the host environment does not change, and the result does not
|
|
109
|
+
* contain them. The function throws when restic does not start. A failed command gives
|
|
110
|
+
* `success: false`.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const executable = await findRestic();
|
|
115
|
+
* const result = await runRestic(
|
|
116
|
+
* { path: ["forget"], args: { "keep-daily": 14, prune: true } },
|
|
117
|
+
* { executable, env: { RESTIC_REPOSITORY: repository, RESTIC_PASSWORD: password } },
|
|
118
|
+
* );
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export async function runRestic(command: ResticCommand, options: ResticRunOptions = {}): Promise<ResticResult> {
|
|
122
|
+
const executable = options.executable ?? process.env["RESTIC_EXE"] ?? "restic";
|
|
123
|
+
const json = command.json ?? true;
|
|
124
|
+
const run = options.run ?? runCommand;
|
|
125
|
+
const result = await run(executable, {
|
|
126
|
+
args: resticArgv({ ...command, json }),
|
|
127
|
+
cwd: options.cwd,
|
|
128
|
+
timeout: options.timeout,
|
|
129
|
+
signal: options.signal,
|
|
130
|
+
env: options.env === undefined ? undefined : { ...options.env },
|
|
131
|
+
...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
|
|
132
|
+
});
|
|
133
|
+
if (result.error) throw new Error(`restic did not start: ${result.error}`);
|
|
134
|
+
const parsed = json ? parseJsonOutput(result.stdout) : undefined;
|
|
135
|
+
return {
|
|
136
|
+
exitCode: result.exitCode,
|
|
137
|
+
success: result.exitCode === 0,
|
|
138
|
+
stdout: result.stdout,
|
|
139
|
+
stderr: result.stderr,
|
|
140
|
+
...(parsed !== undefined ? { json: parsed } : {}),
|
|
141
|
+
...(result.truncated !== undefined ? { truncated: result.truncated } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The settings of the host for the procedures of this package. The procedure input cannot change them. */
|
|
146
|
+
export interface ResticProceduresOptions {
|
|
147
|
+
/** The variables for the restic process only, for example `RESTIC_PASSWORD`. Refer to `ResticRunOptions.env`. */
|
|
148
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
149
|
+
/** The function that starts restic (default: `runCommand`). A test gives a fake. */
|
|
150
|
+
readonly run?: ResticRunner | undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* This function gives the procedures of this package with the settings of the host. With `env`,
|
|
155
|
+
* each restic process gets those variables, and the caller of `restic.run` does not see them.
|
|
156
|
+
*/
|
|
157
|
+
export function createResticProcedures(options: ResticProceduresOptions = {}): AnyProcedure[] {
|
|
158
|
+
const run = createProcedure()
|
|
159
|
+
.path(["restic", "run"])
|
|
160
|
+
.input(zodAdapter<RunInput>(RunInputSchema))
|
|
161
|
+
.output(zodAdapter<RunOutput>(RunOutputSchema))
|
|
162
|
+
.meta({
|
|
163
|
+
description: "Run a restic command (backup, snapshots, restore, forget, check)",
|
|
164
|
+
args: ["path"],
|
|
165
|
+
shorts: { cwd: "C", timeout: "t" },
|
|
166
|
+
output: "json",
|
|
167
|
+
// A backup reads files, a restore writes them, and forget and prune delete snapshots. A
|
|
168
|
+
// repository of type sftp or rclone starts that program.
|
|
169
|
+
effects: { reads: ["fs", "network", "process"], writes: ["fs", "network", "process"], destructive: true },
|
|
170
|
+
})
|
|
171
|
+
.handler(
|
|
172
|
+
async (input: RunInput, ctx?: ProcedureContext): Promise<RunOutput> =>
|
|
173
|
+
runRestic(input, { executable: input.executable, env: options.env, cwd: input.cwd, timeout: input.timeout, signal: ctx?.signal, run: options.run }),
|
|
174
|
+
)
|
|
175
|
+
.build() as AnyProcedure;
|
|
176
|
+
return [run];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const resticProcedures: AnyProcedure[] = createResticProcedures();
|
|
101
180
|
|
|
102
|
-
export const
|
|
181
|
+
export const resticRunProcedure: AnyProcedure = resticProcedures[0]!;
|