aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext')
-rw-r--r--modules/ext/webdav.js300
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';