From 77a61792960d1233035d5695ab165c6cccac71ae Mon Sep 17 00:00:00 2001 From: Alexander Sabino <32822107+asabino2@users.noreply.github.com> Date: Sat, 9 May 2026 16:22:59 +0100 Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.0.5;=20adicio?= =?UTF-8?q?na=20modal=20de=20navegador=20de=20diret=C3=B3rios=20e=20nova?= =?UTF-8?q?=20rota=20API=20para=20listar=20subdiret=C3=B3rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++++-- package.json | 2 +- public/app.js | 62 +++++++++++++++++++++++++++ public/index.html | 27 +++++++++++- public/styles.css | 105 ++++++++++++++++++++++++++++++++++++++++++++++ src/server.js | 24 +++++++++++ 6 files changed, 231 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1002b02..c98bae5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + @@ -18,11 +18,21 @@ > ⚠️ **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.0.2** +Versão atual: **0.0.5** --- -## � Changelog### [0.0.4] — Correções de bugs +## � Changelog + +### [0.0.5] — 2026-05-09 + +#### Adicionado +- **Navegador de diretórios:** no modal de criação/edição de Storage Location, o campo Diretório ganhou um botão de pesquisa (ícone de pasta). Ao clicar, abre um popup que lista os diretórios do servidor, permitindo navegar hierarquicamente e selecionar o caminho desejado sem precisar digitá-lo manualmente. +- **Rota `GET /api/browse-dirs`:** nova rota protegida que aceita o parâmetro `path` e retorna os subdiretórios não-ocultos do caminho informado, junto ao caminho pai e ao caminho atual. + +--- + +### [0.0.4] — Correções de bugs #### Corrigido - **Progresso do backup:** contador de arquivos processados ultrapassava o total porque `find -type f` contava apenas arquivos regulares, enquanto o `tar -v` emite uma linha por entrada (incluindo diretórios e symlinks). Corrigido removendo `-type f` do comando `find`. diff --git a/package.json b/package.json index 66d9f90..fd00261 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockerbackup-app", - "version": "0.0.4", + "version": "0.0.5", "description": "Aplicacao web para backup e restauracao de volumes Docker", "main": "src/server.js", "scripts": { diff --git a/public/app.js b/public/app.js index fb7bdb2..d39a174 100644 --- a/public/app.js +++ b/public/app.js @@ -239,6 +239,68 @@ elements.storageLocationsList?.addEventListener('click', async (e) => { } }); +// ─── Directory Browser Modal ────────────────────────────── +let _dirBrowserCurrentPath = '/'; + +function openDirBrowser() { + const initial = elements.storageLocationDir.value.trim() || '/'; + _dirBrowserCurrentPath = initial; + const modal = document.querySelector('#dirBrowserModal'); + modal.classList.remove('hidden'); + modal.setAttribute('aria-hidden', 'false'); + loadDirBrowserPath(initial); +} + +function closeDirBrowser() { + const modal = document.querySelector('#dirBrowserModal'); + modal.classList.add('hidden'); + modal.setAttribute('aria-hidden', 'true'); +} + +async function loadDirBrowserPath(dirPath) { + _dirBrowserCurrentPath = dirPath; + document.querySelector('#dirBrowserCurrent').textContent = dirPath; + const list = document.querySelector('#dirBrowserList'); + list.innerHTML = '

Carregando…
'; + + let data; + try { + data = await api(`/api/browse-dirs?path=${encodeURIComponent(dirPath)}`); + } catch (err) { + list.innerHTML = `
${escapeHtml(err.message)}
`; + return; + } + + const folderSvg = ``; + const upSvg = ``; + + let html = ''; + if (data.parent !== null) { + html += `
${upSvg}.. (subir)
`; + } + if (data.dirs.length === 0 && data.parent === null) { + html += '
Nenhum subdiretório encontrado.
'; + } else { + html += data.dirs.map((d) => + `
${folderSvg}${escapeHtml(d.name)}
` + ).join(''); + } + list.innerHTML = html; + + list.querySelectorAll('.dir-browser-item[data-dir-path]').forEach((item) => { + item.addEventListener('click', () => loadDirBrowserPath(item.dataset.dirPath)); + }); +} + +document.querySelector('#browseDirBtn')?.addEventListener('click', openDirBrowser); +document.querySelector('#dirBrowserClose')?.addEventListener('click', closeDirBrowser); +document.querySelector('#dirBrowserCancel')?.addEventListener('click', closeDirBrowser); +document.querySelector('#dirBrowserBackdrop')?.addEventListener('click', closeDirBrowser); +document.querySelector('#dirBrowserSelect')?.addEventListener('click', () => { + elements.storageLocationDir.value = _dirBrowserCurrentPath; + closeDirBrowser(); +}); + // ─── Full Backup Picker Modal ───────────────────────────── function askFullBackupSelection(fullBackups, profileName) { elements.fullBackupPickerOptions.innerHTML = fullBackups.map((b) => ` diff --git a/public/index.html b/public/index.html index 3916f60..e08d50e 100644 --- a/public/index.html +++ b/public/index.html @@ -453,7 +453,12 @@
- +
+ + +
@@ -463,6 +468,26 @@
+ + + diff --git a/public/styles.css b/public/styles.css index 11c36ca..99237e5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1109,6 +1109,111 @@ button, input, select { .volume-blocked { opacity: 0.5; cursor: not-allowed; } +/* --- Dir Input Group ------------------------------------- */ +.dir-input-group { + display: flex; + gap: 6px; + align-items: center; +} + +.dir-input-group input { + flex: 1; + min-width: 0; +} + +.dir-browse-btn { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 6px 10px; +} + +/* --- Dir Browser Modal ----------------------------------- */ +#dirBrowserModal { + z-index: 80; +} + +.dir-browser-card { + width: min(520px, calc(100vw - 24px)); + max-height: min(75vh, 560px); + display: flex; + flex-direction: column; + gap: 10px; +} + +.dir-browser-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 12px; + color: var(--text-muted); + min-height: 34px; +} + +.dir-browser-current-path { + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + color: var(--text); + word-break: break-all; +} + +.dir-browser-list { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; + min-height: 120px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 4px; +} + +.dir-browser-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 13px; + color: var(--text); + transition: background 0.1s; + user-select: none; +} + +.dir-browser-item:hover { + background: var(--bg); +} + +.dir-browser-item--up { + color: var(--text-muted); + font-style: italic; +} + +.dir-browser-item svg { + flex-shrink: 0; + color: var(--accent, #3b82f6); +} + +.dir-browser-item--up svg { + color: var(--text-muted); +} + +.dir-browser-empty { + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 13px; + padding: 24px 0; +} + /* --- Responsive ------------------------------------------ */ @media (max-width: 900px) { :root { --sidebar-w: 60px; } diff --git a/src/server.js b/src/server.js index 47aea06..fd9e182 100644 --- a/src/server.js +++ b/src/server.js @@ -479,6 +479,30 @@ async function main() { } }); + app.get('/api/browse-dirs', authMiddleware, async (request, response) => { + try { + const rawPath = typeof request.query.path === 'string' ? request.query.path : '/'; + const dirPath = path.resolve('/', rawPath.replace(/\0/g, '')); + + let entries; + try { + entries = await fs.readdir(dirPath, { withFileTypes: true }); + } catch { + return response.status(400).json({ error: 'Não foi possível ler o diretório.' }); + } + + const dirs = entries + .filter((e) => e.isDirectory() && !e.name.startsWith('.')) + .map((e) => ({ name: e.name, path: path.join(dirPath, e.name) })) + .sort((a, b) => a.name.localeCompare(b.name)); + + const parent = dirPath !== '/' ? path.dirname(dirPath) : null; + response.json({ current: dirPath, parent, dirs }); + } catch (error) { + response.status(500).json({ error: error.message }); + } + }); + app.listen(config.port, () => { console.log(`Docker Backup app ouvindo na porta ${config.port}`); });