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,19 +418,41 @@ 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);
// Gerencia o .snar assim como o script shell usa --listed-incremental=$dirbackup/backup.snar: // Detecta se o container tem GNU tar (--listed-incremental é extensão GNU).
// - Full: remove o .snar anterior do container para forçar snapshot limpo. // Containers Alpine/BusyBox usam o fallback --newer-mtime.
// - Incremental: injeta o .snar salvo no diretório de backup de volta no container. const hasGnuTar = await this.dockerService.containerHasGnuTar(containerId);
if (runMode === 'full') { pushLog(`GNU tar detectado no container: ${hasGnuTar ? 'sim' : 'nao (usando --newer-mtime como fallback)'}.`, 'preparando');
await this.dockerService.runContainerCommand(containerId, `rm -f ${shellQuote(snarInContainer)}`).catch(() => null);
pushLog('Backup full: snapshot incremental anterior removido.', 'preparando'); let tarIncrementalFlag = '';
if (hasGnuTar) {
// 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.
// - Incremental: injeta o .snar salvo no diretório de backup de volta no container.
if (runMode === 'full') {
await this.dockerService.runContainerCommand(containerId, `rm -f ${shellQuote(snarInContainer)}`).catch(() => null);
pushLog('Backup full: snapshot incremental anterior removido.', 'preparando');
} else {
try {
await fs.access(absoluteSnapshotPath);
await this.dockerService.putSnarToContainer(containerId, absoluteSnapshotPath, snarInContainer);
pushLog('Backup incremental: snapshot anterior restaurado no container.', 'preparando');
} catch {
pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando');
}
}
tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`;
} else { } else {
try { // Fallback para containers sem GNU tar: --newer-mtime baseado no timestamp do último backup.
await fs.access(absoluteSnapshotPath); if (runMode === 'incremental') {
await this.dockerService.putSnarToContainer(containerId, absoluteSnapshotPath, snarInContainer); const lastTime = await this.store.getLastContainerBackupTime(profile.id, containerId);
pushLog('Backup incremental: snapshot anterior restaurado no container.', 'preparando'); if (lastTime) {
} catch { const unixSec = Math.floor(new Date(lastTime).getTime() / 1000);
pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando'); tarIncrementalFlag = `--newer-mtime=@${unixSec}`;
pushLog(`Backup incremental (--newer-mtime): arquivos modificados apos ${lastTime}.`, 'preparando');
} else {
pushLog('Aviso: sem backup anterior, gerando full.', 'preparando');
}
} }
} }
@ -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,9 +495,11 @@ 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.
const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false); if (hasGnuTar) {
if (snarSaved) { const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false);
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando'); if (snarSaved) {
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
}
} }
} finally { } finally {
if (tempStarted) { if (tempStarted) {

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) {