From f7c996bc70ae5d24173725104c4e041fb8ee8dfd Mon Sep 17 00:00:00 2001 From: Alexander Sabino <32822107+asabino2@users.noreply.github.com> Date: Sat, 9 May 2026 22:07:49 +0100 Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.1.1;=20corrig?= =?UTF-8?q?e=20bugs=20cr=C3=ADticos=20no=20restore=20de=20backups=20e=20ad?= =?UTF-8?q?iciona=20limpeza=20de=20volumes=20antes=20da=20restaura=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++++++++++-- package.json | 2 +- public/app.js | 25 +++++++++++++++----- src/backupService.js | 56 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 86 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 39e952f..0c78a83 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + @@ -18,11 +18,23 @@ > ⚠️ **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.0** +Versão atual: **0.1.1** --- ## � Changelog +### [0.1.1] — 2026-05-09 + +#### Corrigido +- **Bug crítico no restore de backups:** o comando `tar --listed-incremental=/dev/null` era usado na extração, o que apagava arquivos já restaurados pelo backup full ao aplicar incrementais subsequentes, deixando os volumes vazios. Substituído por extração simples (`tar -xzf`), que sobrepõe corretamente o full + cada incremental. +- **Restore via helper container agora emite logs:** o `runHelper` do restore passou a receber `onOutput`, de modo que cada linha do `tar` e das etapas de limpeza aparece no log de progresso. +- **Limpeza de volumes antes do restore (caminho Docker nativo):** antes de aplicar os archives via `putArchive`, um helper container monta os mesmos volumes do container alvo e remove o conteúdo anterior, garantindo estado consistente pós-restore. +- **Limpeza antes do restore de container inteiro (caminho Docker nativo):** o container é iniciado temporariamente para executar a limpeza do filesystem antes de restaurar via `putArchive`. + +#### Adicionado +- **Log de progresso do restore na aba Backups:** a aba Backups agora exibe o card de progresso (com barras de progresso, etapa atual e log detalhado) durante a execução de um restore, da mesma forma que a aba Profiles já fazia. O card desaparece e a tabela é atualizada ao término. +- **Atualização automática da aba Backups após restore:** ao concluir o restore com a aba Backups visível, a tabela é recarregada automaticamente para refletir o estado atual dos backups. + ### [0.1.0] — 2026-05-09 #### Adicionado diff --git a/package.json b/package.json index 0ae94af..9433bcd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockerbackup-app", - "version": "0.1.0", + "version": "0.1.1", "description": "Aplicacao web para backup e restauracao de volumes Docker", "main": "src/server.js", "scripts": { diff --git a/public/app.js b/public/app.js index 4e5c8a4..ac30f77 100644 --- a/public/app.js +++ b/public/app.js @@ -465,6 +465,7 @@ async function renderBackupsView() {

${escapeHtml(profile.name)}

${escapeHtml(String(totalBackups))} backup(s) +
@@ -474,6 +475,8 @@ async function renderBackupsView() { `; }).join(''); + + renderAllRunProgress(); } function renderBackupRow(b, profile, isFull) { @@ -988,14 +991,17 @@ function progressBar(percent) { function renderRunProgress(profileId) { const run = state.activeRuns.get(profileId); - const host = document.querySelector(`[data-run-progress="${profileId}"]`); - if (!host) { + // Atualiza TODOS os elementos com o atributo (pode existir na aba Profiles E na aba Backups). + const hosts = document.querySelectorAll(`[data-run-progress="${CSS.escape(profileId)}"]`); + if (!hosts.length) { return; } if (!run || !run.progress) { - host.innerHTML = ''; - host.classList.add('hidden'); + for (const host of hosts) { + host.innerHTML = ''; + host.classList.add('hidden'); + } return; } @@ -1009,8 +1015,7 @@ function renderRunProgress(profileId) { const operation = run?.kind === 'restore' || run?.progress?.operation === 'restore' ? 'restore' : 'backup'; const operationTitle = operation === 'restore' ? 'Progresso do restore' : 'Progresso do backup'; - host.classList.remove('hidden'); - host.innerHTML = ` + const progressHtml = `
${escapeHtml(operationTitle)} @@ -1055,6 +1060,11 @@ function renderRunProgress(profileId) {
`; + + for (const host of hosts) { + host.classList.remove('hidden'); + host.innerHTML = progressHtml; + } } function renderAllRunProgress() { @@ -1358,6 +1368,9 @@ async function handleProfileAction(event) { const run = await pollRun(profileId, start.runId); state.activeRuns.delete(profileId); await loadProfiles(); + if (!document.querySelector('#view-backups')?.classList.contains('hidden')) { + await renderBackupsView(); + } if (run.status === 'error') { showToast(run.error || 'Falha durante a execucao do restore.', true); diff --git a/src/backupService.js b/src/backupService.js index 67dcbcf..5660ca4 100644 --- a/src/backupService.js +++ b/src/backupService.js @@ -895,6 +895,29 @@ class BackupService { await fs.access(path.posix.join(backupRoot, entry.archiveRelativePath)); } + // Limpar volumes antes do restore para garantir estado consistente. + // Usa helper container que monta os mesmos volumes do container alvo. + pushLog('Limpando volumes antes do restore.', 'preparando'); + const cleanupHelperBinds = []; + const cleanupHelperCmds = ['set -e']; + for (const [idx, mount] of currentMounts.entries()) { + if (!restorePaths.includes(mount.destination)) continue; + const src = mount.type === 'volume' ? mount.name : mount.source; + cleanupHelperBinds.push(`${src}:/cleanvol/m${idx}`); + cleanupHelperCmds.push(`find /cleanvol/m${idx} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true`); + } + if (cleanupHelperBinds.length) { + await this.dockerService.runHelper({ + binds: cleanupHelperBinds, + cmd: cleanupHelperCmds.join(' && '), + onOutput: (line) => { + const normalized = String(line || '').trim(); + if (normalized) pushLog(normalized, 'preparando'); + }, + }); + } + pushLog('Volumes limpos. Iniciando restauracao dos archives.', 'preparando'); + // Restaurar via Docker API (putArchive) — funciona com container parado. // O archive foi gerado com -C / incluindo os caminhos relativos dos volumes, // portanto o destino do putArchive e sempre /. @@ -927,6 +950,19 @@ class BackupService { await fs.access(path.posix.join(backupRoot, entry.archiveRelativePath)); } + // Para escopo container, inicia temporariamente para limpeza antes do restore. + pushLog('Iniciando container temporariamente para limpeza do filesystem.', 'preparando'); + await this.dockerService.repairAndStartContainer(targetEntry.containerId); + try { + await this.dockerService.runContainerCommand( + targetEntry.containerId, + 'find / -mindepth 1 -maxdepth 1 -not -path "/proc" -not -path "/sys" -not -path "/dev" -not -path "/run" -exec rm -rf -- {} + 2>/dev/null; true', + ); + } finally { + await this.dockerService.stopContainer(targetEntry.containerId).catch(() => null); + } + pushLog('Filesystem limpo. Restaurando archives.', 'preparando'); + for (const [index, entry] of chain.entries()) { const absoluteArchivePath = path.posix.join(backupRoot, entry.archiveRelativePath); @@ -972,9 +1008,14 @@ class BackupService { `find ${shellQuote(`/restore/m${index}`)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +` )); - const restoreCommands = chain.map((entry) => ( - `tar --listed-incremental=/dev/null -xzf ${shellQuote(`/backuproot/${entry.archiveRelativePath}`)} -C /restore` - )); + // Sem --listed-incremental: extração simples sobrepos­crita de arquivos. + // O --listed-incremental=/dev/null (modo snapshot vazio) causava deleção incorreta + // de arquivos já restaurados pelo backup full ao aplicar os incrementais seguintes. + const restoreCommands = []; + for (const [index, entry] of chain.entries()) { + restoreCommands.push(`echo "[${index + 1}/${chain.length}] Aplicando: ${entry.archiveRelativePath}" 1>&2`); + restoreCommands.push(`tar -xzf ${shellQuote(`/backuproot/${entry.archiveRelativePath}`)} -C /restore`); + } try { if (wasRunning) { @@ -982,7 +1023,14 @@ class BackupService { } const cmd = ['set -eu', ...cleanupCommands, ...restoreCommands].join(' && '); - await this.dockerService.runHelper({ binds, cmd }); + await this.dockerService.runHelper({ + binds, + cmd, + onOutput: (line) => { + const normalized = String(line || '').trim(); + if (normalized) pushLog(normalized, 'restaurando'); + }, + }); } finally { if (wasRunning) { await this.dockerService.startContainer(targetEntry.containerId).catch(() => null);
DataTipoStatusContainersAções