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>
|
||||||
|
|
||||||
<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
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue