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:
parent
971293cacf
commit
77a6179296
16
README.md
16
README.md
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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) => `
|
||||
|
|
|
|||
|
|
@ -453,7 +453,12 @@
|
|||
</div>
|
||||
<div class="form-field">
|
||||
<label for="storageLocationDir">Diretório</label>
|
||||
<input id="storageLocationDir" type="text" placeholder="/srv/docker-backups" required />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue