From 83e749065440031048f2bf2d567e9c9e95a9a004 Mon Sep 17 00:00:00 2001 From: Alexander Sabino <32822107+asabino2@users.noreply.github.com> Date: Sat, 9 May 2026 23:15:33 +0100 Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.1.4;=20corrig?= =?UTF-8?q?e=20bugs=20no=20restore=20incremental=20e=20aprimora=20estat?= =?UTF-8?q?=C3=ADsticas=20de=20restaura=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++++++-- package.json | 2 +- src/backupService.js | 43 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b238087..c18f7cb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + @@ -18,11 +18,18 @@ > ⚠️ **AVISO CRÍTICO:** Aplicação em estágio inicial de desenvolvimento. Não use em produção — há risco de perda de dados. -Versão atual: **0.1.3** +Versão atual: **0.1.4** --- ## � Changelog +### [0.1.4] — 2026-05-09 + +#### Corrigido +- **Restore incremental não apagava arquivos deletados entre backups:** `tar -xzf` sem `--listed-incremental` não honra informações de deleção embutidas no archive incremental. Corrigido: archives com `mode === 'incremental'` agora são extraídos com `tar --listed-incremental=/dev/null -xzvf`, que instrui o tar a ler o snapshot embutido e remover arquivos que foram deletados entre o backup anterior e o incremental. O archive full continua usando extração simples. +- **Estatísticas do restore sempre zeradas:** o objeto `restoreStats` era criado mas nunca preenchido. Corrigido: o comando `tar` passou a usar o flag `-v` (verbose), que imprime cada arquivo extraído no stdout; o callback `onOutput` agora recebe o parâmetro `streamName` e incrementa `restoreStats.created` a cada linha do stdout, resultando no total real de arquivos restaurados no toast de conclusão. +- **Path não-nativo (fora do Docker) referenciava `restorePaths` indefinido:** a variável `restorePaths` era usada no filtro de mounts do path não-nativo sem ter sido declarada nesse escopo. Corrigido: o path não-nativo agora computa `restorePathsNonNative` a partir de `chain[0].backupPaths` (igual à lógica do path nativo). + ### [0.1.3] — 2026-05-09 #### Corrigido diff --git a/package.json b/package.json index 6655695..4a59dae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockerbackup-app", - "version": "0.1.3", + "version": "0.1.4", "description": "Aplicacao web para backup e restauracao de volumes Docker", "main": "src/server.js", "scripts": { diff --git a/src/backupService.js b/src/backupService.js index 885dc6a..9ab1f53 100644 --- a/src/backupService.js +++ b/src/backupService.js @@ -935,10 +935,16 @@ class BackupService { // permissão/ownership) ao extrair em volumes. Com set -e/&&, o incremental // nunca seria aplicado porque o full aborta a cadeia. Tratar código 1 como // sucesso e apenas falhar com código >= 2. + // + // Para archives incrementais: --listed-incremental=/dev/null instrui o tar a + // ler o snapshot embutido no archive e APAGAR arquivos que foram removidos + // entre o backup anterior e este incremental (comportamento delta correto). + // Para o archive full: extração simples, sem remoção de arquivos existentes. for (const [index, entry] of chain.entries()) { const archivePath = `${helperBackupRoot}/${entry.archiveRelativePath}`; + const tarIncrFlag = entry.mode === 'incremental' ? '--listed-incremental=/dev/null ' : ''; helperCmds.push(`echo "[${index + 1}/${chain.length}] Aplicando: ${entry.archiveRelativePath}" 1>&2`); - helperCmds.push(`tar -xzf ${shellQuote(archivePath)} -C /; RC=$?; [ $RC -le 1 ] || exit $RC`); + helperCmds.push(`tar ${tarIncrFlag}-xzvf ${shellQuote(archivePath)} -C /; RC=$?; [ $RC -le 1 ] || exit $RC`); } // Emite progresso inicial (o helper roda tudo de uma vez, sem granularidade por arquivo). @@ -951,9 +957,16 @@ class BackupService { await this.dockerService.runHelper({ binds: helperBinds, cmd: helperCmds.join('\n'), - onOutput: (line) => { + onOutput: (line, streamName) => { const normalized = String(line || '').trim(); - if (normalized) pushLog(normalized, 'restaurando'); + if (!normalized) return; + if (streamName === 'stdout') { + // Linha do tar -v: cada arquivo extraído (não logar — podem ser muitos) + restoreStats.created++; + } else { + // Linha do tar (warnings) ou echo de progresso (stderr) + pushLog(normalized, 'restaurando'); + } }, }); @@ -1013,11 +1026,16 @@ class BackupService { const binds = [`${backupRoot}:/backuproot:ro`]; const helperCmds = []; + // Limitar restore aos volumes que estavam ativos no backup (mesma lógica do path nativo). + const restorePathsNonNative = (chain[0]?.backupPaths && chain[0].backupPaths.length) + ? chain[0].backupPaths + : currentMounts.map((mount) => mount.destination); + // Montar cada volume/bind no seu path real (igual ao path nativo). // Archives criados pelo helper já têm entries com paths reais (a0/..., var/lib/gitea/...), // então extrair com -C / coloca os arquivos nos volumes montados corretamente. for (const mount of currentMounts) { - if (restorePaths && restorePaths.length > 0 && !restorePaths.includes(mount.destination)) continue; + if (!restorePathsNonNative.includes(mount.destination)) continue; binds.push(`${getMountBindingSource(mount)}:${mount.destination}`); helperCmds.push( `find ${shellQuote(mount.destination)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true` @@ -1026,9 +1044,13 @@ class BackupService { // Extração de cada archive sem set -e/&&: tar frequentemente retorna código 1 // (avisos de permissão/ownership) — aceitar código 1 como sucesso, falhar apenas em >= 2. + // Para archives incrementais: --listed-incremental=/dev/null instrui o tar a ler o + // snapshot embutido e apagar arquivos removidos entre backups (comportamento delta correto). + const restoreStats = { deleted: 0, created: 0, modified: 0 }; for (const [index, entry] of chain.entries()) { + const tarIncrFlag = entry.mode === 'incremental' ? '--listed-incremental=/dev/null ' : ''; helperCmds.push(`echo "[${index + 1}/${chain.length}] Aplicando: ${entry.archiveRelativePath}" 1>&2`); - helperCmds.push(`tar -xzf ${shellQuote(`/backuproot/${entry.archiveRelativePath}`)} -C /; RC=$?; [ $RC -le 1 ] || exit $RC`); + helperCmds.push(`tar ${tarIncrFlag}-xzvf ${shellQuote(`/backuproot/${entry.archiveRelativePath}`)} -C /; RC=$?; [ $RC -le 1 ] || exit $RC`); } try { @@ -1039,9 +1061,14 @@ class BackupService { await this.dockerService.runHelper({ binds, cmd: helperCmds.join('\n'), - onOutput: (line) => { + onOutput: (line, streamName) => { const normalized = String(line || '').trim(); - if (normalized) pushLog(normalized, 'restaurando'); + if (!normalized) return; + if (streamName === 'stdout') { + restoreStats.created++; + } else { + pushLog(normalized, 'restaurando'); + } }, }); } finally { @@ -1050,7 +1077,7 @@ class BackupService { } } - return { stats: null }; + return { stats: restoreStats }; } }