atualiza versão para 0.0.5; adiciona modal de navegador de diretórios e nova rota API para listar subdiretórios

This commit is contained in:
Alexander Sabino 2026-05-09 16:22:59 +01:00
parent 971293cacf
commit 77a6179296
6 changed files with 231 additions and 5 deletions

View File

@ -9,7 +9,7 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/VERSION-0.0.2-blue?style=flat-square" />
<img src="https://img.shields.io/badge/VERSION-0.0.5-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,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**
---
## <20> Changelog### [0.0.4] — Correções de bugs
## <20> 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`.

View File

@ -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": {

View File

@ -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 = '<div class="dir-browser-empty">Carregando…</div>';
let data;
try {
data = await api(`/api/browse-dirs?path=${encodeURIComponent(dirPath)}`);
} catch (err) {
list.innerHTML = `<div class="dir-browser-empty">${escapeHtml(err.message)}</div>`;
return;
}
const folderSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
const upSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><polyline points="15 18 9 12 15 6"/></svg>`;
let html = '';
if (data.parent !== null) {
html += `<div class="dir-browser-item dir-browser-item--up" data-dir-path="${escapeHtml(data.parent)}">${upSvg}<span>.. (subir)</span></div>`;
}
if (data.dirs.length === 0 && data.parent === null) {
html += '<div class="dir-browser-empty">Nenhum subdiretório encontrado.</div>';
} else {
html += data.dirs.map((d) =>
`<div class="dir-browser-item" data-dir-path="${escapeHtml(d.path)}">${folderSvg}<span>${escapeHtml(d.name)}</span></div>`
).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) => `

View File

@ -453,7 +453,12 @@
</div>
<div class="form-field">
<label for="storageLocationDir">Diretório</label>
<div class="dir-input-group">
<input id="storageLocationDir" type="text" placeholder="/srv/docker-backups" required />
<button type="button" id="browseDirBtn" class="btn btn--secondary btn--sm dir-browse-btn" title="Procurar diretório">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
</button>
</div>
</div>
<div class="form-actions">
<button class="btn btn--primary" type="submit">Salvar</button>
@ -463,6 +468,26 @@
</div>
</div>
<!-- Modal: Navegador de Diretórios -->
<div id="dirBrowserModal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" id="dirBrowserBackdrop"></div>
<div class="modal-card dir-browser-card" role="dialog" aria-modal="true" aria-labelledby="dirBrowserTitle">
<div class="modal-header">
<h3 id="dirBrowserTitle">Selecionar Diretório</h3>
<button id="dirBrowserClose" class="btn btn--ghost btn--sm" type="button">Fechar</button>
</div>
<div class="dir-browser-breadcrumb">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span id="dirBrowserCurrent" class="dir-browser-current-path"></span>
</div>
<div id="dirBrowserList" class="dir-browser-list"></div>
<div class="modal-actions">
<button id="dirBrowserSelect" class="btn btn--primary" type="button">Selecionar este diretório</button>
<button id="dirBrowserCancel" class="btn btn--secondary" type="button">Cancelar</button>
</div>
</div>
</div>
<script src="/translations.js"></script>
<script src="/app.js"></script>
</body>

View File

@ -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; }

View File

@ -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}`);
});