added to remote repo
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { resticProcessEnv } from './config.js';
|
||||
|
||||
export class CommandError extends Error {
|
||||
constructor(message, result) {
|
||||
super(message);
|
||||
this.name = 'CommandError';
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnCommand(command, args = [], options = {}) {
|
||||
const {
|
||||
env = process.env,
|
||||
input = null,
|
||||
log = null,
|
||||
ignoreExitCode = false,
|
||||
cwd = process.cwd(),
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env,
|
||||
shell: false,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
log?.(text.trimEnd());
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
log?.(text.trimEnd());
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on('close', (exitCode) => {
|
||||
const result = { stdout, stderr, exitCode };
|
||||
if (exitCode !== 0 && !ignoreExitCode) {
|
||||
reject(new CommandError(stderr.trim() || `${command} exited with ${exitCode}`, result));
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
if (input) {
|
||||
input.on('error', (error) => {
|
||||
child.kill('SIGTERM');
|
||||
reject(error);
|
||||
});
|
||||
input.pipe(child.stdin);
|
||||
} else {
|
||||
child.stdin.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function runRestic(args, options = {}) {
|
||||
return spawnCommand('restic', args, {
|
||||
...options,
|
||||
env: resticProcessEnv(),
|
||||
});
|
||||
}
|
||||
|
||||
export function streamResticDumpToDd(snapshotId, filename, outputPath, log) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const restic = spawn('restic', ['dump', snapshotId, filename], {
|
||||
env: resticProcessEnv(),
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const dd = spawn('dd', [`of=${outputPath}`, 'bs=4M', 'conv=sparse', 'status=none'], {
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
let resticClosed = false;
|
||||
let ddClosed = false;
|
||||
const pipe = new PassThrough();
|
||||
restic.stdout.pipe(pipe).pipe(dd.stdin);
|
||||
|
||||
restic.stderr.on('data', (chunk) => log?.(chunk.toString().trimEnd()));
|
||||
dd.stderr.on('data', (chunk) => log?.(chunk.toString().trimEnd()));
|
||||
restic.on('error', reject);
|
||||
dd.on('error', reject);
|
||||
|
||||
restic.on('close', (code) => {
|
||||
resticClosed = true;
|
||||
if (code !== 0) {
|
||||
errors.push(`restic dump exited with ${code}`);
|
||||
dd.stdin.destroy();
|
||||
}
|
||||
maybeFinish();
|
||||
});
|
||||
|
||||
dd.on('close', (code) => {
|
||||
ddClosed = true;
|
||||
if (code !== 0) {
|
||||
errors.push(`dd exited with ${code}`);
|
||||
}
|
||||
maybeFinish();
|
||||
});
|
||||
|
||||
function maybeFinish() {
|
||||
if (!resticClosed || !ddClosed) return;
|
||||
if (errors.length) {
|
||||
reject(new Error(errors.join('; ')));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user