atualiza versão para 0.1.2; corrige bugs críticos no restore de volumes e ajusta a geração de archives para compatibilidade

This commit is contained in:
Alexander Sabino 2026-05-09 22:27:32 +01:00
parent f7c996bc70
commit 73409523d2
3 changed files with 77 additions and 64 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.1-blue?style=flat-square" /> <img src="https://img.shields.io/badge/VERSION-0.1.2-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,17 @@
> ⚠️ **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.1** Versão atual: **0.1.2**
--- ---
## <20> Changelog ## <20> Changelog
### [0.1.2] — 2026-05-09
#### Corrigido
- **Restore de volumes não restaurava arquivos (bug crítico):** `putArchive` em container parado escreve na camada overlay do container, não nos volumes nomeados. Ao iniciar, o volume montado (vazio após a limpeza) sobrepunha a camada, tornando os arquivos restaurados invisíveis. Corrigido: o restore de volumes agora usa um helper container que monta cada volume no seu path real (ex: `gitea_data:/var/lib/gitea`) e extrai os archives diretamente lá com `tar -xzf ... -C /`.
- **Backup via helper BusyBox gerava archive com paths incompatíveis:** containers sem GNU tar (Alpine/BusyBox) criavam o archive montando volumes em `/payload/m0`, `/payload/m1` etc., gerando entradas como `payload/m0/arquivo`. Isso era incompativel com o restore que esperava paths no formato real (`var/lib/gitea/arquivo`). Corrigido: o helper agora monta cada volume no seu path real no container (ex: `/var/lib/gitea`), gerando o mesmo formato de archive que o GNU tar nativo.
### [0.1.1] — 2026-05-09 ### [0.1.1] — 2026-05-09
#### Corrigido #### Corrigido

View File

@ -1,6 +1,6 @@
{ {
"name": "dockerbackup-app", "name": "dockerbackup-app",
"version": "0.1.1", "version": "0.1.2",
"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

@ -473,10 +473,13 @@ class BackupService {
const helperBackupRoot = `/backuproot_base${selfBind.suffix}`; const helperBackupRoot = `/backuproot_base${selfBind.suffix}`;
const helperBinds = [`${selfBind.source}:/backuproot_base`]; const helperBinds = [`${selfBind.source}:/backuproot_base`];
const helperRelPaths = []; const helperRelPaths = [];
for (const [index, mount] of activeMounts.entries()) { for (const mount of activeMounts) {
const src = mount.type === 'volume' ? mount.name : mount.source; const src = mount.type === 'volume' ? mount.name : mount.source;
helperBinds.push(`${src}:/payload/m${index}:ro`); // Monta no path real do container (ex: /var/lib/gitea) para que o archive
helperRelPaths.push(`payload/m${index}`); // gerado tenha entradas com os paths corretos (ex: var/lib/gitea/...).
// Isso garante compatibilidade com o restore via helper.
helperBinds.push(`${src}:${mount.destination}:ro`);
helperRelPaths.push(toContainerRelPath(mount.destination));
} }
const helperArchivePath = `${helperBackupRoot}/${archiveRelativePath}`; const helperArchivePath = `${helperBackupRoot}/${archiveRelativePath}`;
const helperSnarPath = `${helperBackupRoot}/${snapshotRelativePath}`; const helperSnarPath = `${helperBackupRoot}/${snapshotRelativePath}`;
@ -895,54 +898,67 @@ 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. // Para restore de volumes no modo Docker-nativo, NÃO usamos putArchive.
// Usa helper container que monta os mesmos volumes do container alvo. // putArchive num container parado escreve na camada overlay do container,
pushLog('Limpando volumes antes do restore.', 'preparando'); // não nos volumes nomeados. Quando o container inicia, o volume montado
const cleanupHelperBinds = []; // (agora vazio após a limpeza) sobrepõe a camada → arquivos invisíveis.
const cleanupHelperCmds = ['set -e']; //
for (const [idx, mount] of currentMounts.entries()) { // Solução: helper container que monta os volumes nos seus paths reais
if (!restorePaths.includes(mount.destination)) continue; // (ex: gitea_data:/var/lib/gitea) e extrai o archive com -C /.
const src = mount.type === 'volume' ? mount.name : mount.source; // O archive já tem as entradas nos paths reais (ex: var/lib/gitea/...).
cleanupHelperBinds.push(`${src}:/cleanvol/m${idx}`); const selfBind = await this.dockerService.getSelfBindSource(backupRoot);
cleanupHelperCmds.push(`find /cleanvol/m${idx} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true`); if (!selfBind) {
} throw new Error(
if (cleanupHelperBinds.length) { `Nao foi possivel determinar o source do diretorio de backup (${backupRoot}) para o helper de restore. ` +
await this.dockerService.runHelper({ 'Verifique se o diretorio de backup esta montado via volume ou bind no container da app.'
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 /.
for (const [index, entry] of chain.entries()) {
const absoluteArchivePath = path.posix.join(backupRoot, entry.archiveRelativePath);
onProgress({
step: 'restaurando',
file: {
current: index + 1,
total: chain.length,
currentFile: entry.archiveRelativePath,
percent: Math.round(((index + 1) / chain.length) * 100),
},
percent: Math.round(((index + 1) / chain.length) * 100),
});
pushLog(`Aplicando arquivo ${index + 1}/${chain.length}: ${entry.archiveRelativePath}`, 'restaurando');
await this.dockerService.putCompressedArchiveFromFile(
targetEntry.containerId,
'/',
absoluteArchivePath,
); );
} }
const helperBackupRoot = `/backuproot_base${selfBind.suffix}`;
const helperBinds = [`${selfBind.source}:/backuproot_base:ro`];
const helperCmds = ['set -e'];
// Montar cada volume no seu path real e preparar limpeza.
for (const mount of currentMounts) {
if (!restorePaths.includes(mount.destination)) continue;
const src = mount.type === 'volume' ? mount.name : mount.source;
helperBinds.push(`${src}:${mount.destination}`);
helperCmds.push(
`find ${shellQuote(mount.destination)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true`
);
}
pushLog('Limpando volumes e restaurando archives via helper.', 'preparando');
// Adicionar extração de cada archive.
for (const [index, entry] of chain.entries()) {
const archivePath = `${helperBackupRoot}/${entry.archiveRelativePath}`;
helperCmds.push(`echo "[${index + 1}/${chain.length}] Aplicando: ${entry.archiveRelativePath}" 1>&2`);
helperCmds.push(`tar -xzf ${shellQuote(archivePath)} -C /`);
}
// Emite progresso inicial (o helper roda tudo de uma vez, sem granularidade por arquivo).
onProgress({
step: 'restaurando',
file: { current: 0, total: chain.length, currentFile: null, percent: 0 },
percent: 10,
});
await this.dockerService.runHelper({
binds: helperBinds,
cmd: helperCmds.join(' && '),
onOutput: (line) => {
const normalized = String(line || '').trim();
if (normalized) pushLog(normalized, 'restaurando');
},
});
onProgress({
step: 'finalizando',
file: { current: chain.length, total: chain.length, currentFile: null, percent: 100 },
percent: 100,
});
pushLog('Restore de volumes concluido.', 'finalizando'); pushLog('Restore de volumes concluido.', 'finalizando');
} else { } else {
// Escopo container inteiro. // Escopo container inteiro.
@ -950,19 +966,10 @@ 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. // Para escopo container, restaura via putArchive (escreve na camada do container).
pushLog('Iniciando container temporariamente para limpeza do filesystem.', 'preparando'); // Nota: paths cobertos por volumes nomeados não serão restaurados via esta rota,
await this.dockerService.repairAndStartContainer(targetEntry.containerId); // pois o volume sobrepõe a camada após o container iniciar. Para esses cases,
try { // use o escopo 'volumes' em vez de 'container'.
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);