Adiciona suporte para execução de containers BusyBox sem GNU tar, redirecionando stdout para um arquivo local e gerando o archive diretamente no diretório de backup.
This commit is contained in:
parent
612ea0abe7
commit
424f19c2e2
|
|
@ -443,19 +443,78 @@ class BackupService {
|
||||||
}
|
}
|
||||||
tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`;
|
tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`;
|
||||||
} else {
|
} else {
|
||||||
// Fallback para containers sem GNU tar: --newer-mtime baseado no timestamp do último backup.
|
// Fallback para containers sem GNU tar: usa helper container com GNU tar
|
||||||
|
// montando os volumes diretamente — produz .snar como qualquer outro container.
|
||||||
if (runMode === 'incremental') {
|
if (runMode === 'incremental') {
|
||||||
const lastTime = await this.store.getLastContainerBackupTime(profile.id, containerId);
|
try {
|
||||||
if (lastTime) {
|
await fs.access(absoluteSnapshotPath);
|
||||||
const unixSec = Math.floor(new Date(lastTime).getTime() / 1000);
|
tarIncrementalFlag = `--listed-incremental=${shellQuote('/backuproot/' + snapshotRelativePath)}`;
|
||||||
tarIncrementalFlag = `--newer-mtime=@${unixSec}`;
|
pushLog('Backup incremental via helper: snapshot anterior encontrado.', 'preparando');
|
||||||
pushLog(`Backup incremental (--newer-mtime): arquivos modificados apos ${lastTime}.`, 'preparando');
|
} catch {
|
||||||
} else {
|
pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo via helper.', 'preparando');
|
||||||
pushLog('Aviso: sem backup anterior, gerando full.', 'preparando');
|
tarIncrementalFlag = `--listed-incremental=${shellQuote('/backuproot/' + snapshotRelativePath)}`;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Full: remove .snar anterior para forçar snapshot limpo.
|
||||||
|
await fsp.rm(absoluteSnapshotPath, { force: true }).catch(() => null);
|
||||||
|
tarIncrementalFlag = `--listed-incremental=${shellQuote('/backuproot/' + snapshotRelativePath)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Containers BusyBox sem GNU tar: roda o tar num helper container que TEM GNU tar,
|
||||||
|
// montando os volumes do container alvo diretamente.
|
||||||
|
if (!hasGnuTar && backupScope === 'volumes' && activeMounts.length) {
|
||||||
|
pushLog('Container sem GNU tar: usando helper com GNU tar para gerar archive.', 'gerando-tar');
|
||||||
|
updateFileProgress();
|
||||||
|
|
||||||
|
const helperBinds = [`${backupRoot}:/backuproot`];
|
||||||
|
const helperRelPaths = [];
|
||||||
|
for (const [index, mount] of activeMounts.entries()) {
|
||||||
|
const src = mount.type === 'volume' ? mount.name : mount.source;
|
||||||
|
helperBinds.push(`${src}:/payload/m${index}:ro`);
|
||||||
|
helperRelPaths.push(`payload/m${index}`);
|
||||||
|
}
|
||||||
|
const helperArchivePath = `/backuproot/${archiveRelativePath}`;
|
||||||
|
const helperSnarDir = path.posix.dirname(`/backuproot/${snapshotRelativePath}`);
|
||||||
|
const helperCmd = [
|
||||||
|
'set -u',
|
||||||
|
`mkdir -p ${shellQuote(path.posix.dirname(helperArchivePath))} ${shellQuote(helperSnarDir)}`,
|
||||||
|
`echo "__DBKP_TAR_BEGIN__" 1>&2`,
|
||||||
|
`tar --ignore-failed-read ${tarIncrementalFlag} -czvf ${shellQuote(helperArchivePath)} -C / ${helperRelPaths.map((p) => shellQuote(p)).join(' ')}; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC`,
|
||||||
|
].join('; ');
|
||||||
|
|
||||||
|
await this.dockerService.runHelper({
|
||||||
|
binds: helperBinds,
|
||||||
|
cmd: helperCmd,
|
||||||
|
maxOkExitCode: 1,
|
||||||
|
onOutput: (line, stream) => {
|
||||||
|
const normalizedLine = String(line || '').trim();
|
||||||
|
if (!normalizedLine || stream !== 'stderr' || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!normalizedLine.startsWith('tar:')) {
|
||||||
|
fileCurrent += 1;
|
||||||
|
updateFileProgress(normalizedLine);
|
||||||
|
} else {
|
||||||
|
pushLog(`Aviso do tar: ${normalizedLine}`, 'gerando-tar');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pushLog(`Arquivo gerado via helper: ${absoluteArchivePath}`, 'finalizando');
|
||||||
|
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
|
||||||
|
|
||||||
|
onProgress({
|
||||||
|
containerName,
|
||||||
|
status: 'ok',
|
||||||
|
step: 'concluido',
|
||||||
|
message: 'Backup concluido com sucesso.',
|
||||||
|
percent: 100,
|
||||||
|
file: { current: Math.max(fileCurrent, fileTotal), total: fileTotal, currentFile: null, percent: 100 },
|
||||||
|
});
|
||||||
|
return containerBackup;
|
||||||
|
}
|
||||||
|
|
||||||
const tarParts = [
|
const tarParts = [
|
||||||
'set -u',
|
'set -u',
|
||||||
'umask 077',
|
'umask 077',
|
||||||
|
|
|
||||||
|
|
@ -523,7 +523,10 @@ class DockerService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runHelper({ binds, cmd, onOutput }) {
|
// Igual ao runHelper, mas redireciona stdout para um arquivo local.
|
||||||
|
// Usado para containers BusyBox (sem GNU tar): monta os volumes no helper que TEM GNU tar
|
||||||
|
// e gera o archive + .snar diretamente no diretório de backup.
|
||||||
|
async runHelperStreamToFile({ binds, cmd, targetFilePath, onOutput, maxOkExitCode = 0 }) {
|
||||||
await this.ensureImage();
|
await this.ensureImage();
|
||||||
|
|
||||||
const container = await this.docker.createContainer({
|
const container = await this.docker.createContainer({
|
||||||
|
|
@ -539,6 +542,76 @@ class DockerService {
|
||||||
|
|
||||||
let attachStream;
|
let attachStream;
|
||||||
|
|
||||||
|
try {
|
||||||
|
attachStream = await container.attach({ stream: true, stdout: true, stderr: true });
|
||||||
|
const stdoutStream = new PassThrough();
|
||||||
|
const stderrStream = new PassThrough();
|
||||||
|
this.docker.modem.demuxStream(attachStream, stdoutStream, stderrStream);
|
||||||
|
|
||||||
|
await fsp.mkdir(path.dirname(targetFilePath), { recursive: true });
|
||||||
|
const writeStream = fs.createWriteStream(targetFilePath);
|
||||||
|
stdoutStream.pipe(writeStream);
|
||||||
|
|
||||||
|
const stderrDone = new Promise((resolve) => {
|
||||||
|
let buffer = '';
|
||||||
|
stderrStream.on('data', (chunk) => {
|
||||||
|
const text = chunk.toString('utf8');
|
||||||
|
buffer += text;
|
||||||
|
const parts = buffer.split(/\r?\n/);
|
||||||
|
buffer = parts.pop() || '';
|
||||||
|
for (const line of parts) {
|
||||||
|
if (typeof onOutput === 'function') onOutput(line, 'stderr');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stderrStream.on('end', () => {
|
||||||
|
if (buffer && typeof onOutput === 'function') onOutput(buffer, 'stderr');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.start();
|
||||||
|
const result = await container.wait();
|
||||||
|
|
||||||
|
if (attachStream && typeof attachStream.destroy === 'function') {
|
||||||
|
attachStream.destroy();
|
||||||
|
}
|
||||||
|
stdoutStream.end();
|
||||||
|
stderrStream.end();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
stderrDone,
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
writeStream.on('finish', resolve);
|
||||||
|
writeStream.on('error', reject);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.StatusCode > maxOkExitCode) {
|
||||||
|
throw new Error(`Helper (stream) terminou com codigo ${result.StatusCode}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (attachStream && typeof attachStream.destroy === 'function') {
|
||||||
|
try { attachStream.destroy(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
try { await container.remove({ force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runHelper({ binds, cmd, onOutput }) { await this.ensureImage();
|
||||||
|
|
||||||
|
const container = await this.docker.createContainer({
|
||||||
|
Image: this.helperImage,
|
||||||
|
Cmd: ['sh', '-c', cmd],
|
||||||
|
Tty: false,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: binds,
|
||||||
|
AutoRemove: false,
|
||||||
|
NetworkMode: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let attachStream;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
attachStream = await container.attach({ stream: true, stdout: true, stderr: true });
|
attachStream = await container.attach({ stream: true, stdout: true, stderr: true });
|
||||||
const stdoutStream = new PassThrough();
|
const stdoutStream = new PassThrough();
|
||||||
|
|
@ -585,7 +658,7 @@ class DockerService {
|
||||||
|
|
||||||
output = output.trim();
|
output = output.trim();
|
||||||
|
|
||||||
if (result.StatusCode !== 0) {
|
if (result.StatusCode > maxOkExitCode) {
|
||||||
throw new Error(output || `Helper container terminou com codigo ${result.StatusCode}`);
|
throw new Error(output || `Helper container terminou com codigo ${result.StatusCode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue