atualiza versão para 0.1.1; corrige bugs críticos no restore de backups e adiciona limpeza de volumes antes da restauração
This commit is contained in:
parent
c5b19bb2d5
commit
f7c996bc70
16
README.md
16
README.md
|
|
@ -9,7 +9,7 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/VERSION-0.1.0-blue?style=flat-square" />
|
||||
<img src="https://img.shields.io/badge/VERSION-0.1.1-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/DOCKER-ready-2496ED?style=flat-square&logo=docker&logoColor=white" />
|
||||
<img src="https://img.shields.io/badge/READY-yes-brightgreen?style=flat-square" />
|
||||
|
|
@ -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**
|
||||
|
||||
---
|
||||
|
||||
## <20> 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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -465,6 +465,7 @@ async function renderBackupsView() {
|
|||
<h2 class="card-title">${escapeHtml(profile.name)}</h2>
|
||||
<span class="badge">${escapeHtml(String(totalBackups))} backup(s)</span>
|
||||
</div>
|
||||
<div class="run-progress hidden" data-run-progress="${escapeHtml(profile.id)}"></div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Data</th><th>Tipo</th><th>Status</th><th>Containers</th><th>Ações</th></tr></thead>
|
||||
|
|
@ -474,6 +475,8 @@ async function renderBackupsView() {
|
|||
</div>
|
||||
`;
|
||||
}).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 = `
|
||||
<div class="progress-card">
|
||||
<div class="progress-header">
|
||||
<strong>${escapeHtml(operationTitle)}</strong>
|
||||
|
|
@ -1055,6 +1060,11 @@ function renderRunProgress(profileId) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue