atualiza versão para 0.1.4; corrige bugs no restore incremental e aprimora estatísticas de restauração

This commit is contained in:
Alexander Sabino 2026-05-09 23:15:33 +01:00
parent c9a76d7b12
commit 83e7490654
3 changed files with 45 additions and 11 deletions

View File

@ -9,7 +9,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/VERSION-0.1.3-blue?style=flat-square" /> <img src="https://img.shields.io/badge/VERSION-0.1.4-blue?style=flat-square" />
<img src="https://img.shields.io/badge/NODE.JS-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" /> <img src="https://img.shields.io/badge/NODE.JS-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" />
<img src="https://img.shields.io/badge/DOCKER-ready-2496ED?style=flat-square&logo=docker&logoColor=white" /> <img src="https://img.shields.io/badge/DOCKER-ready-2496ED?style=flat-square&logo=docker&logoColor=white" />
<img src="https://img.shields.io/badge/READY-yes-brightgreen?style=flat-square" /> <img src="https://img.shields.io/badge/READY-yes-brightgreen?style=flat-square" />
@ -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. > ⚠️ **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**
--- ---
## <20> Changelog ## <20> 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 ### [0.1.3] — 2026-05-09
#### Corrigido #### Corrigido

View File

@ -1,6 +1,6 @@
{ {
"name": "dockerbackup-app", "name": "dockerbackup-app",
"version": "0.1.3", "version": "0.1.4",
"description": "Aplicacao web para backup e restauracao de volumes Docker", "description": "Aplicacao web para backup e restauracao de volumes Docker",
"main": "src/server.js", "main": "src/server.js",
"scripts": { "scripts": {

View File

@ -935,10 +935,16 @@ class BackupService {
// permissão/ownership) ao extrair em volumes. Com set -e/&&, o incremental // 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 // nunca seria aplicado porque o full aborta a cadeia. Tratar código 1 como
// sucesso e apenas falhar com código >= 2. // 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()) { for (const [index, entry] of chain.entries()) {
const archivePath = `${helperBackupRoot}/${entry.archiveRelativePath}`; 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(`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). // Emite progresso inicial (o helper roda tudo de uma vez, sem granularidade por arquivo).
@ -951,9 +957,16 @@ class BackupService {
await this.dockerService.runHelper({ await this.dockerService.runHelper({
binds: helperBinds, binds: helperBinds,
cmd: helperCmds.join('\n'), cmd: helperCmds.join('\n'),
onOutput: (line) => { onOutput: (line, streamName) => {
const normalized = String(line || '').trim(); 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 binds = [`${backupRoot}:/backuproot:ro`];
const helperCmds = []; 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). // 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/...), // 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. // então extrair com -C / coloca os arquivos nos volumes montados corretamente.
for (const mount of currentMounts) { 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}`); binds.push(`${getMountBindingSource(mount)}:${mount.destination}`);
helperCmds.push( helperCmds.push(
`find ${shellQuote(mount.destination)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true` `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 // 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. // (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()) { 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(`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 { try {
@ -1039,9 +1061,14 @@ class BackupService {
await this.dockerService.runHelper({ await this.dockerService.runHelper({
binds, binds,
cmd: helperCmds.join('\n'), cmd: helperCmds.join('\n'),
onOutput: (line) => { onOutput: (line, streamName) => {
const normalized = String(line || '').trim(); const normalized = String(line || '').trim();
if (normalized) pushLog(normalized, 'restaurando'); if (!normalized) return;
if (streamName === 'stdout') {
restoreStats.created++;
} else {
pushLog(normalized, 'restaurando');
}
}, },
}); });
} finally { } finally {
@ -1050,7 +1077,7 @@ class BackupService {
} }
} }
return { stats: null }; return { stats: restoreStats };
} }
} }