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 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:
// - 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.
@ -433,6 +441,20 @@ class BackupService {
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 = [
'set -eu',
@ -442,11 +464,11 @@ class BackupService {
if (backupScope === 'container') {
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 {
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)
// para que a cadeia incremental sobreviva a recriações do container.
if (hasGnuTar) {
const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false);
if (snarSaved) {
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
}
}
} finally {
if (tempStarted) {
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.
// Usa tar POSIX em memória, sem depender do tar do sistema.
async putSnarToContainer(containerId, localSnarPath, containerSnarPath) {