From fd72520f994012d5b35f77759a61a8b64740c302 Mon Sep 17 00:00:00 2001 From: Alexander Sabino <32822107+asabino2@users.noreply.github.com> Date: Tue, 12 May 2026 09:41:15 +0100 Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.2.1;=20adicio?= =?UTF-8?q?na=20suporte=20a=20novos=20tipos=20de=20armazenamento=20(FTP,?= =?UTF-8?q?=20SFTP,=20WebDAV,=20Google=20Drive)=20e=20aprimora=20a=20inter?= =?UTF-8?q?face=20de=20gerenciamento=20de=20locais=20de=20armazenamento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++- package.json | 2 +- public/app.js | 148 ++++++++++++++++++++++++++++++++++++++++++++-- public/index.html | 142 +++++++++++++++++++++++++++++++++++++++++--- public/styles.css | 60 +++++++++++++++++++ src/server.js | 51 +++++++++++++++- src/store.js | 21 ++++++- 7 files changed, 420 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9685c1b..28afdf3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@
-
+
@@ -18,11 +18,23 @@
> ⚠️ **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.1.4**
+Versão atual: **0.2.1**
---
-## � Changelog### [0.2.0] — 2026-05-11
+## 📋 Changelog
+
+### [0.2.1] — 2026-05-12
+
+#### Adicionado
+- **Novos tipos de Storage Location:** além do armazenamento local, agora é possível cadastrar destinos remotos: **FTP**, **SFTP**, **WebDAV** e **Google Drive**.
+- **Campos dinâmicos por tipo de storage:** o modal de criação/edição de Storage Location exibe apenas os campos relevantes ao tipo selecionado (host, porta, usuário, senha, caminho remoto, modo passivo, chave privada SSH, URL WebDAV, credenciais OAuth Google Drive).
+- **Badge de tipo na listagem:** cada Storage Location exibe um badge colorido com seu tipo (Local, FTP, SFTP, WebDAV, Google Drive) e o endereço de destino na tabela.
+- **Dropdown de profile atualizado:** o seletor de Storage Location nos Backup Profiles passa a exibir o tipo de cada location junto ao nome.
+
+---
+
+### [0.2.0] — 2026-05-11
#### Adicionado
- **Aba Source (Origens):** nova aba acima de Storage Locations para gerenciar origens de conexão Docker, com suporte a Unix socket, conexão direta (TCP porta 2375) e Docker Agent
diff --git a/package.json b/package.json
index e4701cf..1546360 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dockerbackup-app",
- "version": "0.2.0",
+ "version": "0.2.1",
"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 7aba611..e6a7408 100644
--- a/public/app.js
+++ b/public/app.js
@@ -95,6 +95,22 @@ const elements = {
storageLocationDir: document.querySelector('#storageLocationDir'),
storageLocationIdField: document.querySelector('#storageFormId'),
storageLocationsList: document.querySelector('#storageLocationsList'),
+ // storage type fields
+ storageHost: document.querySelector('#storageHost'),
+ storagePort: document.querySelector('#storagePort'),
+ storageUsername: document.querySelector('#storageUsername'),
+ storagePassword: document.querySelector('#storagePassword'),
+ storageRemotePath: document.querySelector('#storageRemotePath'),
+ storagePassive: document.querySelector('#storagePassive'),
+ storagePrivateKey: document.querySelector('#storagePrivateKey'),
+ storageWebdavUrl: document.querySelector('#storageWebdavUrl'),
+ storageWebdavUsername: document.querySelector('#storageWebdavUsername'),
+ storageWebdavPassword: document.querySelector('#storageWebdavPassword'),
+ storageWebdavRemotePath: document.querySelector('#storageWebdavRemotePath'),
+ storageGdriveClientId: document.querySelector('#storageGdriveClientId'),
+ storageGdriveClientSecret: document.querySelector('#storageGdriveClientSecret'),
+ storageGdriveRefreshToken: document.querySelector('#storageGdriveRefreshToken'),
+ storageGdriveFolderId: document.querySelector('#storageGdriveFolderId'),
sourceFormModal: document.querySelector('#sourceFormModal'),
sourceForm: document.querySelector('#sourceForm'),
sourceFormId: document.querySelector('#sourceFormId'),
@@ -196,14 +212,39 @@ function renderStorageLocationsList() {
return;
}
+ const STORAGE_TYPE_LABELS = {
+ 'local': 'Local',
+ 'ftp': 'FTP',
+ 'sftp': 'SFTP',
+ 'webdav': 'WebDAV',
+ 'google-drive': 'Google Drive',
+ };
+
+ function storageLocationSummary(loc) {
+ const type = loc.type || 'local';
+ if (type === 'local') return `
${escapeHtml(loc.directory || '')}`;
+ if (type === 'ftp' || type === 'sftp') {
+ const host = loc.host ? escapeHtml(loc.host) : '—';
+ const port = loc.port ? `:${loc.port}` : '';
+ const path = loc.remotePath ? escapeHtml(loc.remotePath) : '';
+ return `${host}${port}${path ? '/' + path.replace(/^\//, '') : ''}`;
+ }
+ if (type === 'webdav') return `${escapeHtml(loc.url || '')}`;
+ if (type === 'google-drive') {
+ return loc.folderId ? `folder: ${escapeHtml(loc.folderId)}` : 'Drive raiz';
+ }
+ return '';
+ }
+
list.innerHTML = `
| Nome | Diretório | Ações | ||
|---|---|---|---|---|
| Nome | Tipo | Destino | Ações | |
| ${escapeHtml(loc.name)} | -${escapeHtml(loc.directory)} |
+ ${escapeHtml(STORAGE_TYPE_LABELS[loc.type || 'local'] || loc.type || 'local')} | +${storageLocationSummary(loc)} | @@ -218,17 +259,63 @@ function populateStorageLocationDropdown() { const select = elements.storageLocationSelect; if (!select) return; const current = select.value; + const STORAGE_TYPE_LABELS = { 'local': 'Local', 'ftp': 'FTP', 'sftp': 'SFTP', 'webdav': 'WebDAV', 'google-drive': 'Google Drive' }; select.innerHTML = '' + - state.storageLocations.map((loc) => - `` - ).join(''); + state.storageLocations.map((loc) => { + const typeLabel = STORAGE_TYPE_LABELS[loc.type || 'local'] || loc.type || 'local'; + const detail = (loc.type === 'local' || !loc.type) ? (loc.directory || '') : + (loc.host || loc.url || loc.clientId || ''); + return ``; + }).join(''); if (current) select.value = current; } +function updateStorageTypeFields(type) { + const sections = { + local: document.querySelector('#storageFieldsLocal'), + ftpSftp: document.querySelector('#storageFieldsFtpSftp'), + ftpOnly: document.querySelector('#storageFieldsFtpOnly'), + sftpOnly: document.querySelector('#storageFieldsSftpOnly'), + webdav: document.querySelector('#storageFieldsWebdav'), + gdrive: document.querySelector('#storageFieldsGdrive'), + }; + + const show = (el) => el?.classList.remove('hidden'); + const hide = (el) => el?.classList.add('hidden'); + + hide(sections.local); + hide(sections.ftpSftp); + hide(sections.ftpOnly); + hide(sections.sftpOnly); + hide(sections.webdav); + hide(sections.gdrive); + + if (type === 'local') { + show(sections.local); + // Update port placeholder when switching to local doesn't apply + } else if (type === 'ftp') { + show(sections.ftpSftp); + show(sections.ftpOnly); + if (elements.storagePort && !elements.storagePort.value) elements.storagePort.placeholder = '21'; + } else if (type === 'sftp') { + show(sections.ftpSftp); + show(sections.sftpOnly); + if (elements.storagePort && !elements.storagePort.value) elements.storagePort.placeholder = '22'; + } else if (type === 'webdav') { + show(sections.webdav); + } else if (type === 'google-drive') { + show(sections.gdrive); + } +} + function openStorageModal() { document.querySelector('#storageModalTitle').textContent = 'Novo Local de Armazenamento'; elements.storageLocationForm.reset(); elements.storageLocationIdField.value = ''; + // Reset to local type + const localRadio = elements.storageLocationForm.querySelector('input[name="storageType"][value="local"]'); + if (localRadio) localRadio.checked = true; + updateStorageTypeFields('local'); elements.storageLocationFormModal.classList.remove('hidden'); elements.storageLocationFormModal.setAttribute('aria-hidden', 'false'); } @@ -240,12 +327,58 @@ function closeStorageModal() { async function saveStorageLocation(event) { event.preventDefault(); + + const form = elements.storageLocationForm; + const selectedTypeInput = form.querySelector('input[name="storageType"]:checked'); + const type = selectedTypeInput ? selectedTypeInput.value : 'local'; + const payload = { id: elements.storageLocationIdField.value || undefined, name: elements.storageLocationName.value.trim(), - directory: elements.storageLocationDir.value.trim(), + type, }; + if (type === 'local') { + payload.directory = elements.storageLocationDir.value.trim(); + if (!payload.directory) { + showToast('Informe o diretório para armazenamento local.', true); + return; + } + } else if (type === 'ftp' || type === 'sftp') { + payload.host = elements.storageHost.value.trim(); + payload.port = elements.storagePort.value ? Number(elements.storagePort.value) : (type === 'ftp' ? 21 : 22); + payload.username = elements.storageUsername.value.trim(); + payload.password = elements.storagePassword.value; + payload.remotePath = elements.storageRemotePath.value.trim(); + if (!payload.host || !payload.username) { + showToast('Informe host e usuário para ' + type.toUpperCase() + '.', true); + return; + } + if (type === 'ftp') { + payload.passive = elements.storagePassive.checked; + } else { + payload.privateKey = elements.storagePrivateKey.value; + } + } else if (type === 'webdav') { + payload.url = elements.storageWebdavUrl.value.trim(); + payload.username = elements.storageWebdavUsername.value.trim(); + payload.password = elements.storageWebdavPassword.value; + payload.remotePath = elements.storageWebdavRemotePath.value.trim(); + if (!payload.url) { + showToast('Informe a URL do servidor WebDAV.', true); + return; + } + } else if (type === 'google-drive') { + payload.clientId = elements.storageGdriveClientId.value.trim(); + payload.clientSecret = elements.storageGdriveClientSecret.value; + payload.refreshToken = elements.storageGdriveRefreshToken.value; + payload.folderId = elements.storageGdriveFolderId.value.trim(); + if (!payload.clientId || !payload.clientSecret || !payload.refreshToken) { + showToast('Informe Client ID, Client Secret e Refresh Token para Google Drive.', true); + return; + } + } + try { await api('/api/storage-locations', { method: 'POST', @@ -265,6 +398,9 @@ document.querySelector('#storageModalClose')?.addEventListener('click', closeSto elements.storageLocationFormModal?.addEventListener('click', (e) => { if (e.target.closest('[data-action="close-storage-modal"]')) closeStorageModal(); }); +elements.storageLocationFormModal?.addEventListener('change', (e) => { + if (e.target.name === 'storageType') updateStorageTypeFields(e.target.value); +}); elements.storageLocationForm?.addEventListener('submit', saveStorageLocation); elements.storageLocationsList?.addEventListener('click', async (e) => { const btn = e.target.closest('[data-storage-action="delete"]'); diff --git a/public/index.html b/public/index.html index cd2c09d..c12affb 100644 --- a/public/index.html +++ b/public/index.html @@ -538,26 +538,152 @@ |