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>
|
||||||
|
|
||||||
<p align="center">
|
<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/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,23 @@
|
||||||
|
|
||||||
> ⚠️ **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.0**
|
Versão atual: **0.1.1**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## <20> Changelog
|
## <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
|
### [0.1.0] — 2026-05-09
|
||||||
|
|
||||||
#### Adicionado
|
#### Adicionado
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dockerbackup-app",
|
"name": "dockerbackup-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"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": {
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,7 @@ async function renderBackupsView() {
|
||||||
<h2 class="card-title">${escapeHtml(profile.name)}</h2>
|
<h2 class="card-title">${escapeHtml(profile.name)}</h2>
|
||||||
<span class="badge">${escapeHtml(String(totalBackups))} backup(s)</span>
|
<span class="badge">${escapeHtml(String(totalBackups))} backup(s)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="run-progress hidden" data-run-progress="${escapeHtml(profile.id)}"></div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead><tr><th>Data</th><th>Tipo</th><th>Status</th><th>Containers</th><th>Ações</th></tr></thead>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
renderAllRunProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBackupRow(b, profile, isFull) {
|
function renderBackupRow(b, profile, isFull) {
|
||||||
|
|
@ -988,14 +991,17 @@ function progressBar(percent) {
|
||||||
|
|
||||||
function renderRunProgress(profileId) {
|
function renderRunProgress(profileId) {
|
||||||
const run = state.activeRuns.get(profileId);
|
const run = state.activeRuns.get(profileId);
|
||||||
const host = document.querySelector(`[data-run-progress="${profileId}"]`);
|
// Atualiza TODOS os elementos com o atributo (pode existir na aba Profiles E na aba Backups).
|
||||||
if (!host) {
|
const hosts = document.querySelectorAll(`[data-run-progress="${CSS.escape(profileId)}"]`);
|
||||||
|
if (!hosts.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!run || !run.progress) {
|
if (!run || !run.progress) {
|
||||||
|
for (const host of hosts) {
|
||||||
host.innerHTML = '';
|
host.innerHTML = '';
|
||||||
host.classList.add('hidden');
|
host.classList.add('hidden');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1009,8 +1015,7 @@ function renderRunProgress(profileId) {
|
||||||
const operation = run?.kind === 'restore' || run?.progress?.operation === 'restore' ? 'restore' : 'backup';
|
const operation = run?.kind === 'restore' || run?.progress?.operation === 'restore' ? 'restore' : 'backup';
|
||||||
const operationTitle = operation === 'restore' ? 'Progresso do restore' : 'Progresso do backup';
|
const operationTitle = operation === 'restore' ? 'Progresso do restore' : 'Progresso do backup';
|
||||||
|
|
||||||
host.classList.remove('hidden');
|
const progressHtml = `
|
||||||
host.innerHTML = `
|
|
||||||
<div class="progress-card">
|
<div class="progress-card">
|
||||||
<div class="progress-header">
|
<div class="progress-header">
|
||||||
<strong>${escapeHtml(operationTitle)}</strong>
|
<strong>${escapeHtml(operationTitle)}</strong>
|
||||||
|
|
@ -1055,6 +1060,11 @@ function renderRunProgress(profileId) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
for (const host of hosts) {
|
||||||
|
host.classList.remove('hidden');
|
||||||
|
host.innerHTML = progressHtml;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAllRunProgress() {
|
function renderAllRunProgress() {
|
||||||
|
|
@ -1358,6 +1368,9 @@ async function handleProfileAction(event) {
|
||||||
const run = await pollRun(profileId, start.runId);
|
const run = await pollRun(profileId, start.runId);
|
||||||
state.activeRuns.delete(profileId);
|
state.activeRuns.delete(profileId);
|
||||||
await loadProfiles();
|
await loadProfiles();
|
||||||
|
if (!document.querySelector('#view-backups')?.classList.contains('hidden')) {
|
||||||
|
await renderBackupsView();
|
||||||
|
}
|
||||||
|
|
||||||
if (run.status === 'error') {
|
if (run.status === 'error') {
|
||||||
showToast(run.error || 'Falha durante a execucao do restore.', true);
|
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));
|
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.
|
// Restaurar via Docker API (putArchive) — funciona com container parado.
|
||||||
// O archive foi gerado com -C / incluindo os caminhos relativos dos volumes,
|
// O archive foi gerado com -C / incluindo os caminhos relativos dos volumes,
|
||||||
// portanto o destino do putArchive e sempre /.
|
// portanto o destino do putArchive e sempre /.
|
||||||
|
|
@ -927,6 +950,19 @@ class BackupService {
|
||||||
await fs.access(path.posix.join(backupRoot, entry.archiveRelativePath));
|
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()) {
|
for (const [index, entry] of chain.entries()) {
|
||||||
const absoluteArchivePath = path.posix.join(backupRoot, entry.archiveRelativePath);
|
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 -- {} +`
|
`find ${shellQuote(`/restore/m${index}`)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`
|
||||||
));
|
));
|
||||||
|
|
||||||
const restoreCommands = chain.map((entry) => (
|
// Sem --listed-incremental: extração simples sobreposcrita de arquivos.
|
||||||
`tar --listed-incremental=/dev/null -xzf ${shellQuote(`/backuproot/${entry.archiveRelativePath}`)} -C /restore`
|
// 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 {
|
try {
|
||||||
if (wasRunning) {
|
if (wasRunning) {
|
||||||
|
|
@ -982,7 +1023,14 @@ class BackupService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmd = ['set -eu', ...cleanupCommands, ...restoreCommands].join(' && ');
|
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 {
|
} finally {
|
||||||
if (wasRunning) {
|
if (wasRunning) {
|
||||||
await this.dockerService.startContainer(targetEntry.containerId).catch(() => null);
|
await this.dockerService.startContainer(targetEntry.containerId).catch(() => null);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue