Add hierarchical customer file browser with folder ZIP downloads

- /u/:token/files now lists a single directory level with type info,
  /u/:token/zip streams a ZIP of any folder (whole customer dir by
  default). Both paths apply realpath containment so a symlink dropped
  into the customer folder via WebDAV cannot escape — listing now 404s
  on out-of-base symlinks the same way the file download already did.
- Frontend gets breadcrumbs, folder navigation and per-folder/whole-
  current-folder ZIP buttons; UNC \\HOST@PORT\DavWWWRoot\ form is
  derived from the configured WebDAV URL and shown next to it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-16 14:29:56 +02:00
parent d4c1d1f4bf
commit fd5e917249
4 changed files with 197 additions and 38 deletions
+25 -5
View File
@@ -292,7 +292,11 @@
</table>
</div>
</div>
<p class="small" style="margin-top:.75rem">WebDAV-Server: <code id="webdavUrl"></code> — Login mit deinem eigenen Benutzer.</p>
<p class="small" style="margin-top:.75rem">
WebDAV-Server: <code id="webdavUrl"></code><br>
Windows-UNC: <code id="webdavUnc"></code>
— Login mit deinem eigenen Benutzer.
</p>
</section>
<!-- USERS TAB -->
@@ -500,6 +504,24 @@ function show(view) {
document.getElementById(id).style.display = id === view ? '' : 'none';
}
}
function deriveUnc(webdavUrl) {
try {
let s = String(webdavUrl || '').trim();
if (!s) return null;
if (!/^[a-z]+:\/\//i.test(s)) s = 'http://' + s;
const u = new URL(s);
const host = u.hostname;
const port = u.port || '1900';
if (!host) return null;
return `\\\\${host}@${port}\\DavWWWRoot\\`;
} catch { return null; }
}
function setWebdavDisplay(url) {
document.getElementById('webdavUrl').textContent = url;
const unc = deriveUnc(url);
document.getElementById('webdavUnc').textContent = unc || '';
}
function fmtSize(n) {
if (!n) return '0 B';
if (n < 1024) return n + ' B';
@@ -551,8 +573,7 @@ async function bootstrap() {
} else {
document.getElementById('createCustomerCard').style.display = '';
}
document.getElementById('webdavUrl').textContent =
(status.webdav_url || '').trim() || `webdav://${location.hostname}:1900/`;
setWebdavDisplay((status.webdav_url || '').trim() || `webdav://${location.hostname}:1900/`);
show('view-app');
loadCustomers();
}
@@ -818,8 +839,7 @@ document.getElementById('settingsForm').addEventListener('submit', async (e) =>
logo_height_px: getSlider('logoHeight'),
});
// Refresh main view in case the WebDAV-URL display needs an update.
document.getElementById('webdavUrl').textContent =
(fd.get('webdav_url') || '').trim() || `webdav://${location.hostname}:1900/`;
setWebdavDisplay((fd.get('webdav_url') || '').trim() || `webdav://${location.hostname}:1900/`);
const msg = document.getElementById('settingsMsg');
msg.textContent = '✓ Gespeichert';
setTimeout(() => msg.textContent = '', 2000);