Adiciona suporte para detecção de GNU tar no DockerService e ajusta gerenciamento de snapshots no BackupService

This commit is contained in:
Alexander Sabino 2026-05-07 10:55:15 +01:00
parent a8036507bb
commit 52bec257ee
2 changed files with 52 additions and 17 deletions

View File

@ -418,6 +418,14 @@ class BackupService {
const snarInContainer = containerSnapshotPath(profile.id, containerId, backupScope); const snarInContainer = containerSnapshotPath(profile.id, containerId, backupScope);
const absoluteSnapshotPath = path.posix.join(backupRoot, snapshotRelativePath); const absoluteSnapshotPath = path.posix.join(backupRoot, snapshotRelativePath);
// Detecta se o container tem GNU tar (--listed-incremental é extensão GNU).
// Containers Alpine/BusyBox usam o fallback --newer-mtime.
const hasGnuTar = await this.dockerService.containerHasGnuTar(containerId);
pushLog(`GNU tar detectado no container: ${hasGnuTar ? 'sim' : 'nao (usando --newer-mtime como fallback)'}.`, 'preparando');
let tarIncrementalFlag = '';
if (hasGnuTar) {
// Gerencia o .snar assim como o script shell usa --listed-incremental=$dirbackup/backup.snar: // Gerencia o .snar assim como o script shell usa --listed-incremental=$dirbackup/backup.snar:
// - Full: remove o .snar anterior do container para forçar snapshot limpo. // - Full: remove o .snar anterior do container para forçar snapshot limpo.
// - Incremental: injeta o .snar salvo no diretório de backup de volta no container. // - Incremental: injeta o .snar salvo no diretório de backup de volta no container.
@ -433,6 +441,20 @@ class BackupService {
pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando'); pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando');
} }
} }
tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`;
} else {
// Fallback para containers sem GNU tar: --newer-mtime baseado no timestamp do último backup.
if (runMode === 'incremental') {
const lastTime = await this.store.getLastContainerBackupTime(profile.id, containerId);
if (lastTime) {
const unixSec = Math.floor(new Date(lastTime).getTime() / 1000);
tarIncrementalFlag = `--newer-mtime=@${unixSec}`;
pushLog(`Backup incremental (--newer-mtime): arquivos modificados apos ${lastTime}.`, 'preparando');
} else {
pushLog('Aviso: sem backup anterior, gerando full.', 'preparando');
}
}
}
const tarParts = [ const tarParts = [
'set -eu', 'set -eu',
@ -442,11 +464,11 @@ class BackupService {
if (backupScope === 'container') { if (backupScope === 'container') {
tarParts.push( tarParts.push(
`tar --warning=no-file-changed --ignore-failed-read --listed-incremental=${shellQuote(snarInContainer)} -czvf - -C / --exclude=proc --exclude=sys --exclude=dev --exclude=run --exclude=tmp .` `tar --warning=no-file-changed --ignore-failed-read ${tarIncrementalFlag} -czvf - -C / --exclude=proc --exclude=sys --exclude=dev --exclude=run --exclude=tmp .`
); );
} else { } else {
tarParts.push( tarParts.push(
`tar --warning=no-file-changed --ignore-failed-read --listed-incremental=${shellQuote(snarInContainer)} -czvf - -C / ${relSourcePaths.map((item) => shellQuote(item)).join(' ')}` `tar --warning=no-file-changed --ignore-failed-read ${tarIncrementalFlag} -czvf - -C / ${relSourcePaths.map((item) => shellQuote(item)).join(' ')}`
); );
} }
@ -473,10 +495,12 @@ class BackupService {
// Persiste o .snar atualizado no diretório de backup (como o script shell faz com $dirbackup/backup.snar) // Persiste o .snar atualizado no diretório de backup (como o script shell faz com $dirbackup/backup.snar)
// para que a cadeia incremental sobreviva a recriações do container. // para que a cadeia incremental sobreviva a recriações do container.
if (hasGnuTar) {
const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false); const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false);
if (snarSaved) { if (snarSaved) {
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando'); pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
} }
}
} finally { } finally {
if (tempStarted) { if (tempStarted) {
pushLog('Encerrando container apos backup.', 'finalizando'); pushLog('Encerrando container apos backup.', 'finalizando');

View File

@ -437,6 +437,17 @@ class DockerService {
}); });
} }
// Retorna true se o container tem GNU tar (suporta --listed-incremental).
// Containers Alpine/BusyBox retornam false e devem usar --newer-mtime como fallback.
async containerHasGnuTar(containerId) {
try {
const output = await this.runContainerCommand(containerId, 'tar --version 2>/dev/null | head -1');
return /GNU tar/i.test(output);
} catch {
return false;
}
}
// Injeta um arquivo .snar local no container no caminho absoluto informado. // Injeta um arquivo .snar local no container no caminho absoluto informado.
// Usa tar POSIX em memória, sem depender do tar do sistema. // Usa tar POSIX em memória, sem depender do tar do sistema.
async putSnarToContainer(containerId, localSnarPath, containerSnarPath) { async putSnarToContainer(containerId, localSnarPath, containerSnarPath) {