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:
parent
f7c996bc70
commit
73409523d2
10
README.md
10
README.md
|
|
@ -9,7 +9,7 @@
|
|||
</p>
|
||||
|
||||
<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/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,17 @@
|
|||
|
||||
> ⚠️ **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
|
||||
### [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
|
||||
|
||||
#### Corrigido
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dockerbackup-app",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "Aplicacao web para backup e restauracao de volumes Docker",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -473,10 +473,13 @@ class BackupService {
|
|||
const helperBackupRoot = `/backuproot_base${selfBind.suffix}`;
|
||||
const helperBinds = [`${selfBind.source}:/backuproot_base`];
|
||||
const helperRelPaths = [];
|
||||
for (const [index, mount] of activeMounts.entries()) {
|
||||
for (const mount of activeMounts) {
|
||||
const src = mount.type === 'volume' ? mount.name : mount.source;
|
||||
helperBinds.push(`${src}:/payload/m${index}:ro`);
|
||||
helperRelPaths.push(`payload/m${index}`);
|
||||
// Monta no path real do container (ex: /var/lib/gitea) para que o archive
|
||||
// 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 helperSnarPath = `${helperBackupRoot}/${snapshotRelativePath}`;
|
||||
|
|
@ -895,54 +898,67 @@ 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 /.
|
||||
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,
|
||||
// Para restore de volumes no modo Docker-nativo, NÃO usamos putArchive.
|
||||
// putArchive num container parado escreve na camada overlay do container,
|
||||
// não nos volumes nomeados. Quando o container inicia, o volume montado
|
||||
// (agora vazio após a limpeza) sobrepõe a camada → arquivos invisíveis.
|
||||
//
|
||||
// Solução: helper container que monta os volumes nos seus paths reais
|
||||
// (ex: gitea_data:/var/lib/gitea) e extrai o archive com -C /.
|
||||
// O archive já tem as entradas nos paths reais (ex: var/lib/gitea/...).
|
||||
const selfBind = await this.dockerService.getSelfBindSource(backupRoot);
|
||||
if (!selfBind) {
|
||||
throw new Error(
|
||||
`Nao foi possivel determinar o source do diretorio de backup (${backupRoot}) para o helper de restore. ` +
|
||||
'Verifique se o diretorio de backup esta montado via volume ou bind no container da app.'
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
} else {
|
||||
// Escopo container inteiro.
|
||||
|
|
@ -950,19 +966,10 @@ 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');
|
||||
|
||||
// Para escopo container, restaura via putArchive (escreve na camada do container).
|
||||
// Nota: paths cobertos por volumes nomeados não serão restaurados via esta rota,
|
||||
// pois o volume sobrepõe a camada após o container iniciar. Para esses cases,
|
||||
// use o escopo 'volumes' em vez de 'container'.
|
||||
for (const [index, entry] of chain.entries()) {
|
||||
const absoluteArchivePath = path.posix.join(backupRoot, entry.archiveRelativePath);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue