From a58ee6299a484e6a8473e62e1c45b843e473e364 Mon Sep 17 00:00:00 2001 From: Alexander Sabino <32822107+asabino2@users.noreply.github.com> Date: Sun, 10 May 2026 11:13:22 +0100 Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.1.6;=20corrig?= =?UTF-8?q?e=20contagem=20de=20arquivos=20antes=20do=20backup=20e=20garant?= =?UTF-8?q?e=20que=20o=20container=20alvo=20seja=20parado=20durante=20o=20?= =?UTF-8?q?processo=20de=20backup=20de=20volumes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- package.json | 2 +- src/backupService.js | 181 +++++++++++++++++++++---------------------- 3 files changed, 95 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index ff5785b..496855f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + @@ -23,6 +23,12 @@ Versão atual: **0.1.4** --- ## � Changelog +### [0.1.6] — 2026-05-11 + +#### Corrigido +- Contagem de arquivos totais antes do inicio do backup para todos os escopos (container inteiro e volumes), corrigindo a barra de progresso que mostrava 0 no total +- Container alvo agora e parado antes do backup de volumes e reiniciado apos conclusao, evitando inconsistencias nos dados arquivados + ### [0.1.5] — 2026-05-10 #### Adicionado diff --git a/package.json b/package.json index ef0bc96..6e6541a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockerbackup-app", - "version": "0.1.5", + "version": "0.1.6", "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 d0ab4e5..cce0381 100644 --- a/src/backupService.js +++ b/src/backupService.js @@ -390,77 +390,18 @@ class BackupService { pushLog(`Diretorio de backup pronto em ${backupRoot}.`, 'preparando'); const originalRunning = inspect.State?.Running === true; - let tempStarted = false; - try { + + // === Path A: escopo de volumes → helper container (container alvo fica parado) === + if (backupScope === 'volumes') { if (originalRunning) { pushLog('Container ativo detectado. Parando antes do backup.', 'preparando'); await this.dockerService.stopContainer(containerId); } - pushLog('Iniciando container temporariamente para snapshot.', 'preparando'); - await this.dockerService.repairAndStartContainer(containerId); - tempStarted = true; - - const sourcePaths = backupScope === 'container' - ? ['/'] - : activeMounts.map((mount) => mount.destination); - const relSourcePaths = sourcePaths.map((item) => toContainerRelPath(item)); - - if (backupScope === 'volumes') { - pushLog('Contando arquivos para barra de progresso.', 'contando'); - const countCmd = `set -eu; TOTAL=0; for p in ${relSourcePaths.map((item) => shellQuote(item)).join(' ')}; do if [ -e \"/$p\" ]; then C=$(find \"/$p\" 2>/dev/null | wc -l | tr -d \" \" ); TOTAL=$((TOTAL + C)); fi; done; echo \"$TOTAL\"`; - const output = await this.dockerService.runContainerCommand(containerId, countCmd); - const parsed = Number(output.split(/\r?\n/).pop()); - fileTotal = Number.isFinite(parsed) ? parsed : 0; - pushLog(`Total de arquivos identificado: ${fileTotal}.`, 'contando'); - } - const absoluteArchivePath = path.posix.join(backupRoot, archiveRelativePath); - const snarInContainer = containerSnapshotPath(profile.id, containerId, backupScope); const absoluteSnapshotPath = path.posix.join(backupRoot, snapshotRelativePath); - // Detecta se o container tem GNU tar (--listed-incremental é extensão GNU). - // Containers Alpine/BusyBox usam o fallback --newer-mtime. - const hasGnuTar = await this.dockerService.containerHasGnuTar(containerId); - pushLog(`GNU tar detectado no container: ${hasGnuTar ? 'sim' : 'nao (usando --newer-mtime como fallback)'}.`, 'preparando'); - - let tarIncrementalFlag = ''; - - if (hasGnuTar) { - // Gerencia o .snar assim como o script shell usa --listed-incremental=$dirbackup/backup.snar: - // - Full: remove o .snar anterior do container para forçar snapshot limpo. - // - Incremental: injeta o .snar salvo no diretório de backup de volta no container. - if (runMode === 'full') { - await this.dockerService.runContainerCommand(containerId, `rm -f ${shellQuote(snarInContainer)}`).catch(() => null); - pushLog('Backup full: snapshot incremental anterior removido.', 'preparando'); - } else { - try { - await fs.access(absoluteSnapshotPath); - await this.dockerService.putSnarToContainer(containerId, absoluteSnapshotPath, snarInContainer); - pushLog('Backup incremental: snapshot anterior restaurado no container.', 'preparando'); - } catch { - pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando'); - } - } - tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`; - } else { - // Fallback para containers sem GNU tar: usa helper container com GNU tar - // montando os volumes diretamente — produz .snar como qualquer outro container. - if (runMode === 'full') { - // Full: remove .snar anterior para forçar snapshot limpo no helper. - await fs.rm(absoluteSnapshotPath, { force: true }).catch(() => null); - } - // O .snar é gerenciado pelo helper usando helperSnarPath calculado abaixo. - } - - // Containers BusyBox sem GNU tar: roda o tar num helper container que TEM GNU tar, - // montando os volumes do container alvo diretamente. - if (!hasGnuTar && backupScope === 'volumes' && activeMounts.length) { - pushLog('Container sem GNU tar: usando helper com GNU tar para gerar archive.', 'gerando-tar'); - updateFileProgress(); - - // Quando rodando dentro do Docker, backupRoot é um path interno do container da app. - // Precisamos do source real (host path ou volume name) para passar ao helper container. + try { const selfBind = await this.dockerService.getSelfBindSource(backupRoot); if (!selfBind) { throw new Error( @@ -469,18 +410,40 @@ class BackupService { ); } - // suffix é o subpath dentro do volume (ex: /backups quando mount=/app/data e backupRoot=/app/data/backups) const helperBackupRoot = `/backuproot_base${selfBind.suffix}`; const helperBinds = [`${selfBind.source}:/backuproot_base`]; const helperRelPaths = []; for (const mount of activeMounts) { const src = mount.type === 'volume' ? mount.name : mount.source; - // 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. + // Monta no path real do container para que o archive tenha entradas com paths corretos. helperBinds.push(`${src}:${mount.destination}:ro`); helperRelPaths.push(toContainerRelPath(mount.destination)); } + + // Conta arquivos via helper antes de iniciar o tar (container alvo permanece parado). + pushLog('Contando arquivos para barra de progresso.', 'contando'); + const countCmd = `set -eu; TOTAL=0; for p in ${helperRelPaths.map((p) => shellQuote('/' + p)).join(' ')}; do if [ -e "$p" ]; then C=$(find "$p" 2>/dev/null | wc -l | tr -d ' '); TOTAL=$((TOTAL + C)); fi; done; echo "__DBKP_TOTAL__=$TOTAL"`; + await this.dockerService.runHelper({ + binds: helperBinds, + cmd: countCmd, + onOutput: (line) => { + const m = String(line || '').match(/__DBKP_TOTAL__=(\d+)/); + if (m) fileTotal = Number(m[1]) || 0; + }, + }); + pushLog(`Total de arquivos identificado: ${fileTotal}.`, 'contando'); + updateFileProgress(); + + if (runMode === 'full') { + await fs.rm(absoluteSnapshotPath, { force: true }).catch(() => null); + pushLog('Backup full: snapshot incremental anterior removido.', 'preparando'); + } else { + const snarExists = await fs.access(absoluteSnapshotPath).then(() => true).catch(() => false); + pushLog(snarExists + ? 'Backup incremental: snapshot anterior encontrado.' + : 'Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando'); + } + const helperArchivePath = `${helperBackupRoot}/${archiveRelativePath}`; const helperSnarPath = `${helperBackupRoot}/${snapshotRelativePath}`; const helperSnarDir = path.posix.dirname(helperSnarPath); @@ -491,17 +454,14 @@ class BackupService { `tar --ignore-failed-read --listed-incremental=${shellQuote(helperSnarPath)} -czvf ${shellQuote(helperArchivePath)} -C / ${helperRelPaths.map((p) => shellQuote(p)).join(' ')}; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC`, ].join('; '); + pushLog('Iniciando compactacao tar dos volumes via helper.', 'gerando-tar'); await this.dockerService.runHelper({ binds: helperBinds, cmd: helperCmd, maxOkExitCode: 1, onOutput: (line, stream) => { const normalizedLine = String(line || '').trim(); - if (!normalizedLine || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) { - return; - } - // No helper (tar escreve em arquivo): lista de arquivos vai para stdout; - // avisos do tar vão para stderr. + if (!normalizedLine || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) return; if (stream === 'stdout' && !normalizedLine.startsWith('tar:')) { fileCurrent += 1; updateFileProgress(normalizedLine); @@ -527,30 +487,67 @@ class BackupService { file: { current: Math.max(fileCurrent, fileTotal), total: fileTotal, currentFile: null, percent: 100 }, }); return containerBackup; + } finally { + if (originalRunning) { + pushLog('Reiniciando container (estava ativo antes do backup).', 'finalizando'); + await this.dockerService.startContainer(containerId).catch(() => null); + } + } + } + + // === Path B: escopo de container inteiro → iniciar temporariamente para exec === + const absoluteArchivePath = path.posix.join(backupRoot, archiveRelativePath); + const absoluteSnapshotPath = path.posix.join(backupRoot, snapshotRelativePath); + let tempStarted = false; + try { + if (originalRunning) { + pushLog('Container ativo detectado. Parando antes do backup.', 'preparando'); + await this.dockerService.stopContainer(containerId); } + pushLog('Iniciando container temporariamente para snapshot.', 'preparando'); + await this.dockerService.repairAndStartContainer(containerId); + tempStarted = true; + + // Conta arquivos excluindo pseudo-filesystems antes de iniciar o tar. + pushLog('Contando arquivos para barra de progresso.', 'contando'); + const countCmd = `find / \\( -path /proc -o -path /sys -o -path /dev -o -path /run -o -path /tmp \\) -prune -o -print 2>/dev/null | wc -l | tr -d ' '`; + const countOutput = await this.dockerService.runContainerCommand(containerId, countCmd); + const countParsed = Number(countOutput.split(/\r?\n/).filter((l) => l.trim()).pop()); + fileTotal = Number.isFinite(countParsed) ? countParsed : 0; + pushLog(`Total de arquivos identificado: ${fileTotal}.`, 'contando'); + + const snarInContainer = containerSnapshotPath(profile.id, containerId, backupScope); + + // Detecta se o container tem GNU tar (--listed-incremental é extensão GNU). + const hasGnuTar = await this.dockerService.containerHasGnuTar(containerId); + pushLog(`GNU tar detectado no container: ${hasGnuTar ? 'sim' : 'nao (usando --newer-mtime como fallback)'}.`, 'preparando'); + + let tarIncrementalFlag = ''; + if (hasGnuTar) { + if (runMode === 'full') { + await this.dockerService.runContainerCommand(containerId, `rm -f ${shellQuote(snarInContainer)}`).catch(() => null); + pushLog('Backup full: snapshot incremental anterior removido.', 'preparando'); + } else { + try { + await fs.access(absoluteSnapshotPath); + await this.dockerService.putSnarToContainer(containerId, absoluteSnapshotPath, snarInContainer); + pushLog('Backup incremental: snapshot anterior restaurado no container.', 'preparando'); + } catch { + pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando'); + } + } + tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`; + } + + const gnuFlags = hasGnuTar ? '--ignore-failed-read' : ''; const tarParts = [ 'set -u', 'umask 077', 'echo "__DBKP_TAR_BEGIN__" 1>&2', + `tar ${gnuFlags} ${tarIncrementalFlag} -czvf - -C / --exclude=proc --exclude=sys --exclude=dev --exclude=run --exclude=tmp .; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC`, ]; - // --ignore-failed-read é extensão GNU tar — não existe no BusyBox tar (Alpine). - // Usar condicionalmente para evitar aborto silencioso com 0 bytes no arquivo. - const gnuFlags = hasGnuTar ? '--ignore-failed-read' : ''; - - // GNU tar: exit 0 = ok, exit 1 = avisos (arquivos mudaram, permissão negada), exit 2 = erro fatal. - // Aceitamos exit 1 como sucesso para não descartar archives válidos com avisos menores. - if (backupScope === 'container') { - tarParts.push( - `tar ${gnuFlags} ${tarIncrementalFlag} -czvf - -C / --exclude=proc --exclude=sys --exclude=dev --exclude=run --exclude=tmp .; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC` - ); - } else { - tarParts.push( - `tar ${gnuFlags} ${tarIncrementalFlag} -czvf - -C / ${relSourcePaths.map((item) => shellQuote(item)).join(' ')}; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC` - ); - } - updateFileProgress(); pushLog('Iniciando compactacao tar do container.', 'gerando-tar'); @@ -558,10 +555,7 @@ class BackupService { maxOkExitCode: 1, onOutput: (line, stream) => { const normalizedLine = String(line || '').trim(); - if (!normalizedLine || stream !== 'stderr' || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) { - return; - } - + if (!normalizedLine || stream !== 'stderr' || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) return; if (!normalizedLine.startsWith('tar:')) { fileCurrent += 1; updateFileProgress(normalizedLine); @@ -573,8 +567,7 @@ class BackupService { pushLog(`Arquivo gerado: ${absoluteArchivePath}`, 'finalizando'); - // Persiste o .snar atualizado no diretório de backup (como o script shell faz com $dirbackup/backup.snar) - // para que a cadeia incremental sobreviva a recriações do container. + // Persiste o .snar atualizado para sobreviver a recriações do container. if (hasGnuTar) { const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false); if (snarSaved) {