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>
|
||||||
|
|
||||||
<p align="center">
|
<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/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/DOCKER-ready-2496ED?style=flat-square&logo=docker&logoColor=white" />
|
||||||
<img src="https://img.shields.io/badge/READY-yes-brightgreen?style=flat-square" />
|
<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.
|
> ⚠️ **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
|
#### 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`.
|
- **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",
|
"name": "dockerbackup-app",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"description": "Aplicacao web para backup e restauracao de volumes Docker",
|
"description": "Aplicacao web para backup e restauracao de volumes Docker",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
"scripts": {
|
"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 ─────────────────────────────
|
// ─── Full Backup Picker Modal ─────────────────────────────
|
||||||
function askFullBackupSelection(fullBackups, profileName) {
|
function askFullBackupSelection(fullBackups, profileName) {
|
||||||
elements.fullBackupPickerOptions.innerHTML = fullBackups.map((b) => `
|
elements.fullBackupPickerOptions.innerHTML = fullBackups.map((b) => `
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="storageLocationDir">Diretório</label>
|
<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>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn btn--primary" type="submit">Salvar</button>
|
<button class="btn btn--primary" type="submit">Salvar</button>
|
||||||
|
|
@ -463,6 +468,26 @@
|
||||||
</div>
|
</div>
|
||||||
</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="/translations.js"></script>
|
||||||
<script src="/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1109,6 +1109,111 @@ button, input, select {
|
||||||
|
|
||||||
.volume-blocked { opacity: 0.5; cursor: not-allowed; }
|
.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 ------------------------------------------ */
|
/* --- Responsive ------------------------------------------ */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
:root { --sidebar-w: 60px; }
|
: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, () => {
|
app.listen(config.port, () => {
|
||||||
console.log(`Docker Backup app ouvindo na porta ${config.port}`);
|
console.log(`Docker Backup app ouvindo na porta ${config.port}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue