added install script for agent
changed backend to agent
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { PassThrough, Transform } 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 async function resticSnapshotFileSize(snapshotId, filename) {
|
||||
const result = await runRestic(['ls', '--json', snapshotId]);
|
||||
const wanted = `/${filename}`;
|
||||
for (const line of result.stdout.split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.type === 'file' && (entry.path === filename || entry.path === wanted || entry.path?.endsWith(wanted))) {
|
||||
return Number(entry.size);
|
||||
}
|
||||
}
|
||||
const error = new Error(`Could not verify Restic file size for ${filename} in snapshot ${snapshotId}.`);
|
||||
error.status = 500;
|
||||
throw error;
|
||||
}
|
||||
|
||||
export function createProgressStream(totalBytes, onProgress) {
|
||||
let currentBytes = 0;
|
||||
return new Transform({
|
||||
transform(chunk, _encoding, callback) {
|
||||
currentBytes += chunk.length;
|
||||
onProgress?.({
|
||||
currentBytes,
|
||||
totalBytes,
|
||||
percent: totalBytes ? (currentBytes / totalBytes) * 100 : 0,
|
||||
});
|
||||
callback(null, chunk);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function streamResticDumpToDd(snapshotId, filename, outputPath, log, progress = null) {
|
||||
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();
|
||||
let restoreStream = restic.stdout.pipe(pipe);
|
||||
if (progress) {
|
||||
restoreStream = restoreStream.pipe(createProgressStream(progress.totalBytes, progress.onProgress));
|
||||
}
|
||||
restoreStream.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