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)
+
| Data | Tipo | Status | Containers | Ações |
@@ -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 = `
`;
+
+ 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 sobreposcrita 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);