diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-26 06:00:26 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-26 06:00:26 +0200 |
| commit | f4d934d341056142da1212c845eeeb170fd452c2 (patch) | |
| tree | 55dd11c70d1342857537f4b0aeee6ea63d21545e /modules/ext | |
| parent | 4baa484b84c5184286cac2743575251454a5ee2b (diff) | |
auto: 2026-05-26_06:00 changes
Diffstat (limited to 'modules/ext')
| -rw-r--r-- | modules/ext/webdav.js | 300 |
1 files changed, 298 insertions, 2 deletions
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index 63dfb05..89e2115 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -47,6 +47,7 @@ class WebDAVModule { 'webdav-restore': 'Restore VFS from a WebDAV backup file', 'webdav-backup-list': 'List available backup files on WebDAV', 'webdav-explorer': 'webdav-explorer [path] — Interactive remote file browser', + 'webdav-browser': 'webdav-browser [path] — Visual WebDAV file explorer modal', }; } @@ -57,9 +58,15 @@ class WebDAVModule { this.config = JSON.parse(saved); // Try to get password from SecretVault if (window.webx11_secretVault && this.config && this.config.username) { - const encryptedPass = window.webx11_secretVault.decrypt('webdav_pass_' + this.config.username); + let encryptedPass = window.webx11_secretVault.decrypt('webdav_pass_' + this.config.username); if (encryptedPass !== null && encryptedPass !== undefined) { this.config.password = encryptedPass; + } else { + // Fallback to old token key + encryptedPass = window.webx11_secretVault.decrypt('webdav_token_' + this.config.username); + if (encryptedPass !== null && encryptedPass !== undefined) { + this.config.password = encryptedPass; + } } } } @@ -135,7 +142,7 @@ class WebDAVModule { } async _listDirViaAutoIndex(path) { - const baseUrl = this.config.url.replace(/\/+$/, ''); + const baseUrl = this.config.url.replace(/\/+$/, '') + '/'; const urlPath = path ? (path.startsWith('/') ? path : '/' + path) : '/'; // Use __list__ for directory listing const listUrl = baseUrl.replace(/\/spaces\/([^/]+)\//, '/spaces/$1/__list__/') + urlPath.replace(/^\//, ''); @@ -277,6 +284,20 @@ class WebDAVModule { token = localStorage.getItem('webx11_spaces_' + spaceName); } + // Fallback: fetch .token directly from server + if (!token) { + try { + const tokenResp = await fetch('/spaces/' + spaceName + '/.token'); + if (tokenResp.ok) { + token = (await tokenResp.text()).trim(); + // Cache in SecretVault for future use + if (window.webx11_secretVault) { + window.webx11_secretVault.encrypt('webdav_token_' + spaceName, token); + } + } + } catch (e) { /* .token not accessible */ } + } + if (token) { self.config = { url: autoUrl, @@ -500,6 +521,12 @@ class WebDAVModule { } }; + // --- webdav-browser: visual modal file browser --- + this.commands['webdav-browser'] = async (args) => { + const startPath = args[0] || self._browsingPath || '/'; + await self.openWebdavBrowser(startPath); + }; + // --- webdav-explorer: interactive file browser via WebDAV --- this.commands['webdav-explorer'] = async (args) => { const startPath = args[0] || '/'; @@ -524,6 +551,275 @@ class WebDAVModule { }; } + /* ── Visual WebDAV Browser Modal ── */ + + async openWebdavBrowser(path) { + if (!this.config || !this.config.url) { + if (this.terminal) this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning'); + return; + } + this._browsingPath = path; + const self = this; + + // Remove existing modal if any + const existing = document.getElementById('webdav-browser-modal'); + if (existing) existing.remove(); + + // Fetch listing + let entries = []; + let errorMsg = ''; + try { + entries = await this._listDir(path); + } catch (e) { + errorMsg = e.message; + } + + // Build modal + const overlay = document.createElement('div'); + overlay.id = 'webdav-browser-modal'; + overlay.className = 'modal-overlay'; + overlay.style.cssText = 'display:flex; justify-content:center; align-items:center; z-index:999;'; + overlay.onclick = function(e) { if (e.target === this) self.closeWebdavBrowser(); }; + + const content = document.createElement('div'); + content.className = 'modal-content'; + content.style.cssText = 'max-width:650px; max-height:80vh; width:92%;'; + + // Header + const header = document.createElement('div'); + header.className = 'modal-header'; + header.innerHTML = '<span>📁 WebDAV Browser</span><span style="cursor:pointer" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.remove();var t=window.webx11;if(t&&t.inputField)t.inputField.focus();">✕</span>'; + content.appendChild(header); + + // Body + const body = document.createElement('div'); + body.className = 'modal-body'; + body.style.cssText = 'padding:8px; overflow-y:auto; flex-grow:1;'; + + if (errorMsg) { + const errDiv = document.createElement('div'); + errDiv.style.cssText = 'color:var(--error-color); padding:16px; text-align:center;'; + errDiv.textContent = '❌ ' + errorMsg; + body.appendChild(errDiv); + } else if (entries.length === 0) { + const emptyDiv = document.createElement('div'); + emptyDiv.className = 'text-center'; + emptyDiv.style.cssText = 'padding:20px; color:var(--text-muted);'; + emptyDiv.textContent = '(empty) — upload files using the button below'; + body.appendChild(emptyDiv); + } else { + // Sort: dirs first, then files, alphabetically + entries.sort(function(a, b) { + if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; + return a.name.localeCompare(b.name); + }); + + var frag = document.createDocumentFragment(); + entries.forEach(function(entry) { + var item = document.createElement('div'); + item.className = entry.type === 'dir' ? 'file-item dir' : 'file-item'; + item.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:8px; border-bottom:1px solid #111; cursor:pointer; min-height:36px;'; + item.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; }; + item.onmouseout = function() { this.style.background = ''; }; + + var nameSpan = document.createElement('span'); + var icon = entry.type === 'dir' ? '📁' : '📄'; + nameSpan.textContent = icon + ' ' + entry.name + (entry.type === 'dir' ? '/' : ''); + + var sizeSpan = document.createElement('span'); + sizeSpan.style.cssText = 'font-size:0.8em; color:var(--text-muted);'; + if (entry.type === 'file') { + sizeSpan.textContent = self._formatSize(entry.size); + } + + item.appendChild(nameSpan); + item.appendChild(sizeSpan); + + if (entry.type === 'dir') { + item.onclick = function() { self.openWebdavBrowser(entry.href); }; + } else { + item.onclick = function() { self.previewWebdavFile(entry); }; + // Right-click or context menu could download — but for simplicity, long tap or regular click previews + // Double-click to download + item.ondblclick = function() { self.downloadFromWebdav(entry); }; + } + frag.appendChild(item); + }); + body.appendChild(frag); + } + + content.appendChild(body); + + // Footer with breadcrumb + buttons + const footer = document.createElement('div'); + footer.className = 'modal-footer'; + footer.style.cssText = 'display:flex; justify-content:space-between; align-items:center; gap:6px; flex-wrap:wrap; padding:8px 10px;'; + + // Breadcrumb + var breadDiv = document.createElement('div'); + breadDiv.style.cssText = 'font-size:0.8em; color:var(--info-color); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0;'; + var cleanPath = path.replace(/\/+$/, '') || '/'; + var parts = cleanPath.split('/').filter(Boolean); + var breadHtml = '📁 <span style="cursor:pointer;text-decoration:underline;" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.__nav(\'/\');return false;">/</span>'; + var cumulative = ''; + for (var i = 0; i < parts.length; i++) { + cumulative += '/' + parts[i]; + var seg = parts[i]; + breadHtml += '<span style="cursor:pointer;text-decoration:underline;" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.__nav(\'' + cumulative.replace(/'/g, "\\'") + '/\');return false;">' + seg + '</span>'; + if (i < parts.length - 1) breadHtml += ' ▸ '; + } + breadDiv.innerHTML = breadHtml; + // Attach nav function to modal element for breadcrumb clicks + overlay.__nav = function(p) { self.openWebdavBrowser(p); }; + + // Button group + var btnGroup = document.createElement('div'); + btnGroup.style.cssText = 'display:flex; gap:4px; flex-shrink:0;'; + + var uploadBtn = document.createElement('button'); + uploadBtn.className = 'btn'; + uploadBtn.textContent = '⬆ Upload'; + uploadBtn.onclick = function() { self.uploadToWebdav(path); }; + + var refreshBtn = document.createElement('button'); + refreshBtn.className = 'btn'; + refreshBtn.textContent = '🔄'; + refreshBtn.title = 'Refresh'; + refreshBtn.onclick = function() { self.openWebdavBrowser(path); }; + + var closeBtn = document.createElement('button'); + closeBtn.className = 'btn'; + closeBtn.textContent = 'Close'; + closeBtn.onclick = function() { self.closeWebdavBrowser(); }; + + btnGroup.appendChild(uploadBtn); + btnGroup.appendChild(refreshBtn); + btnGroup.appendChild(closeBtn); + + footer.appendChild(breadDiv); + footer.appendChild(btnGroup); + content.appendChild(footer); + + overlay.appendChild(content); + document.body.appendChild(overlay); + + this.terminal.print('📂 Opened WebDAV browser: ' + path, 'info'); + } + + closeWebdavBrowser() { + var modal = document.getElementById('webdav-browser-modal'); + if (modal) modal.remove(); + if (this.terminal && this.terminal.inputField) this.terminal.inputField.focus(); + this.terminal.print('WebDAV browser closed', 'text-muted'); + } + + async previewWebdavFile(entry) { + try { + var resp = await this._webdavRequest('GET', entry.href); + if (!resp.ok) { this.terminal.print('Failed to fetch: HTTP ' + resp.status, 'error'); return; } + var content = await resp.text(); + var isHtml = entry.name.match(/\.html?$/i); + var isImage = entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i); + var size = content.length; + + if (size > 512000 && !isHtml && !isImage) { + // Large non-html file — offer download instead + this.terminal.print(entry.name + ' is ' + this._formatSize(size) + ' — double-click to download instead of preview.', 'warning'); + this.terminal.print('--- ' + entry.name + ' (first 2000 chars) ---'); + this.terminal.print(content.substring(0, 2000)); + if (content.length > 2000) this.terminal.print('...(truncated ' + this._formatSize(size) + ' total)'); + return; + } + + if (isImage) { + var dataUrl = 'data:image/' + (entry.name.split('.').pop()) + ';base64,' + btoa(unescape(encodeURIComponent(content))); + // Use preview modal + var previewTitle = document.getElementById('preview-title'); + var previewIframe = document.getElementById('preview-iframe'); + if (previewTitle && previewIframe) { + previewTitle.textContent = '🖼 ' + entry.name; + previewIframe.srcdoc = '<img src="' + dataUrl + '" style="max-width:100%; max-height:100%; display:block; margin:auto; background:var(--bg-color);">'; + document.getElementById('preview-modal').style.display = 'flex'; + return; + } + } + + if (isHtml) { + var previewTitle = document.getElementById('preview-title'); + var previewIframe = document.getElementById('preview-iframe'); + if (previewTitle && previewIframe) { + previewTitle.textContent = '📄 ' + entry.name; + previewIframe.srcdoc = content; + document.getElementById('preview-modal').style.display = 'flex'; + return; + } + } + + // Text file — print to terminal + this.terminal.print('--- ' + entry.name + ' (' + this._formatSize(size) + ') ---', 'info'); + this.terminal.print(content); + this.terminal.print('--- End: ' + entry.name + ' ---', 'text-muted'); + } catch (e) { + this.terminal.print('Preview error: ' + e.message, 'error'); + } + } + + uploadToWebdav(currentPath) { + var self = this; + var input = document.createElement('input'); + input.type = 'file'; + input.onchange = async function(e) { + var file = e.target.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = async function(evt) { + var basePath = currentPath.replace(/\/+$/, ''); + var remotePath = basePath + '/' + file.name; + var parentDir = remotePath.split('/').slice(0, -1).join('/'); + try { + await self._ensureDir(parentDir); + var resp = await self._webdavRequest('PUT', remotePath, { + body: evt.target.result, + headers: { 'Content-Type': 'application/octet-stream' } + }); + if (resp.ok || resp.status === 201 || resp.status === 204) { + self.terminal.print('⬆ Uploaded: ' + file.name + ' (' + self._formatSize(file.size) + ')', 'success'); + self.openWebdavBrowser(currentPath); + } else { + self.terminal.print('Upload failed: HTTP ' + resp.status, 'error'); + } + } catch (e) { + self.terminal.print('Upload error: ' + e.message, 'error'); + } + }; + reader.readAsText(file); + }; + input.click(); + } + + async downloadFromWebdav(entry) { + try { + var resp = await this._webdavRequest('GET', entry.href); + if (!resp.ok) { this.terminal.print('Download failed: HTTP ' + resp.status, 'error'); return; } + var content = await resp.text(); + var blob = new Blob([content], { type: 'application/octet-stream' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = entry.name; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + setTimeout(function() { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 200); + this.terminal.print('⬇ Downloaded: ' + entry.name + ' (' + this._formatSize(content.length) + ')', 'success'); + } catch (e) { + this.terminal.print('Download error: ' + e.message, 'error'); + } + } + _formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; |