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 = ` - + ${state.storageLocations.map((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 @@
NomeDiretórioAções
NomeTipoDestinoAções
${escapeHtml(loc.name)}${escapeHtml(loc.directory)}${escapeHtml(STORAGE_TYPE_LABELS[loc.type || 'local'] || loc.type || 'local')}${storageLocationSummary(loc)}