From 73409523d278135a95fd0836dfd594d3ffa201b6 Mon Sep 17 00:00:00 2001 From: Alexander Sabino <32822107+asabino2@users.noreply.github.com> Date: Sat, 9 May 2026 22:27:32 +0100 Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.1.2;=20corrig?= =?UTF-8?q?e=20bugs=20cr=C3=ADticos=20no=20restore=20de=20volumes=20e=20aj?= =?UTF-8?q?usta=20a=20gera=C3=A7=C3=A3o=20de=20archives=20para=20compatibi?= =?UTF-8?q?lidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +++- package.json | 2 +- src/backupService.js | 129 +++++++++++++++++++++++-------------------- 3 files changed, 77 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 0c78a83..0e72efb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@
-
+
@@ -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**
---
## � 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
diff --git a/package.json b/package.json
index 9433bcd..26715b6 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/backupService.js b/src/backupService.js
index 5660ca4..3ddd674 100644
--- a/src/backupService.js
+++ b/src/backupService.js
@@ -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);