From faf21d54d14070b7a747cfb6feadda15d845a881 Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Thu, 29 Jan 2026 20:11:45 +0100 Subject: [PATCH] added download tracking; other minor UI improvements --- expressjs/src/server.js | 195 +++++++++++++++++++++++++++++++++------- 1 file changed, 161 insertions(+), 34 deletions(-) diff --git a/expressjs/src/server.js b/expressjs/src/server.js index 4769f11..9359f24 100644 --- a/expressjs/src/server.js +++ b/expressjs/src/server.js @@ -68,6 +68,7 @@ db.serialize(() => { detail TEXT, created_at INTEGER NOT NULL )`); + db.run('ALTER TABLE uploads ADD COLUMN downloads INTEGER DEFAULT 0', (err) => { /* ignore if column exists */ }); db.run('CREATE INDEX IF NOT EXISTS admin_logs_event_idx ON admin_logs(event)'); db.run('CREATE INDEX IF NOT EXISTS admin_logs_created_idx ON admin_logs(created_at)'); db.run(`CREATE TABLE IF NOT EXISTS users ( @@ -559,10 +560,15 @@ function renderPage(title, body, mainClass = '') { } button.secondary:hover { border-color: var(--text-muted); background: var(--bg); } - .row { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } - .row .card { margin: 0; padding: 1.25rem; text-align: center; } - .row .card strong { display: block; font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.05em; margin-bottom: 0.5rem; } - .row .card .muted { font-size: 1.5rem; color: var(--text-main); font-weight: 600; } + .row table { margin-bottom: 0; } + .row td { padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); } + .row tr:last-child td { border-bottom: none; } + .row strong { color: var(--text-muted); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em; } + .row .muted { font-size: 1.1rem; color: var(--text-main); font-weight: 600; text-align: right; } + + .truncate { max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .stack { width: 100%; margin-top: 0.25rem; } + .stack button { width: 100%; justify-content: center; } table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.925rem; } th { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); color: var(--text-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; } @@ -889,12 +895,38 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { const stats = `
-
Aktive Uploads
${activeCount.count}
-
Aktive Größe
${formatBytes(activeBytes.total)}
-
Aktive Nutzer
${distinctOwners.count}
-
Uploads gesamt
${totalUploads.count}
-
Löschungen gesamt
${totalDeletes.count}
-
Letztes Cleanup
${lastCleanup.ts ? formatTimestamp(lastCleanup.ts) : '—'}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Aktive Uploads${activeCount.count}
Aktive Größe${formatBytes(activeBytes.total)}
Aktive Nutzer${distinctOwners.count}
Uploads gesamt${totalUploads.count}
Downloads gesamt${await get('SELECT SUM(downloads) as count FROM uploads').then(r => r.count || 0)}
Löschungen gesamt${totalDeletes.count}
Letztes Cleanup${lastCleanup.ts ? formatTimestamp(lastCleanup.ts) : '—'}
`; @@ -912,10 +944,10 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { const fileHref = encodeURI(fileUrl); return ` - ${escapeHtml(item.owner)} + ${escapeHtml(item.owner)} -
${escapeHtml(item.original_name)}
-
${escapeHtml(item.stored_name)}
+
${escapeHtml(item.original_name)}
+
${escapeHtml(item.stored_name)}
${formatBytes(item.size_bytes)} @@ -932,6 +964,9 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { +
+ +
`; @@ -974,12 +1009,11 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
-

Aktive Uploads

- ${allUploads.length ? ` +

Aktuelle Uploads

+ ${uploads.length ? ` - @@ -987,11 +1021,84 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => { - ${adminUploadsRows} + ${rows}
Nutzer Datei Größe Läuft ab
- ` : '
Keine aktiven Uploads.
'} + ` : '
Noch keine Uploads.
'}
+ + `; res.send(renderPage('Adminübersicht', body, 'wide')); }); @@ -1603,9 +1710,9 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => { const fileHref = encodeURI(fileUrl); return ` - -
${escapeHtml(item.original_name)}
-
${escapeHtml(item.stored_name)}
+ +
${escapeHtml(item.original_name)}
+
${escapeHtml(item.stored_name)}
${formatBytes(item.size_bytes)} @@ -1622,9 +1729,9 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => { - - - +
+ +
`; @@ -1670,7 +1777,6 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => { Größe Läuft ab Aktionen - Link @@ -1716,18 +1822,34 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => { const originalText = button.textContent; button.addEventListener('click', async () => { const path = button.dataset.path || ''; + const name = button.dataset.name || 'Download'; const url = window.location.origin + path; + try { - await navigator.clipboard.writeText(url); + // Try to write rich text (HTML link) + plain text fallback + const html = `${name}`; + const blobHtml = new Blob([html], { type: 'text/html' }); + const blobText = new Blob([url], { type: 'text/plain' }); + const data = [new ClipboardItem({ + 'text/html': blobHtml, + 'text/plain': blobText, + })]; + await navigator.clipboard.write(data); button.textContent = 'Kopiert!'; } catch (err) { - const helper = document.createElement('textarea'); - helper.value = url; - document.body.appendChild(helper); - helper.select(); - document.execCommand('copy'); - document.body.removeChild(helper); - button.textContent = 'Kopiert!'; + // Fallback to simple text copy if Clipboard Item API fails or is not supported + try { + await navigator.clipboard.writeText(url); + button.textContent = 'Kopiert!'; + } catch (fallbackErr) { + const helper = document.createElement('textarea'); + helper.value = url; + document.body.appendChild(helper); + helper.select(); + document.execCommand('copy'); + document.body.removeChild(helper); + button.textContent = 'Kopiert!'; + } } setTimeout(() => { button.textContent = originalText; @@ -1841,12 +1963,13 @@ app.get('/_share/:filename', async (req, res) => { return; } - const row = await get('SELECT original_name, stored_path FROM uploads WHERE stored_name = ?', [filename]); + const row = await get('SELECT id, original_name, stored_path FROM uploads WHERE stored_name = ?', [filename]); if (!row) { // If not found in DB, check if it exists on disk (legacy or manual files) const filePath = path.join(shareDir, filename); if (fs.existsSync(filePath)) { + // Log download for legacy/manual files if needed, or just skip res.download(filePath, filename); // Fallback: download with stored name return; } @@ -1854,6 +1977,10 @@ app.get('/_share/:filename', async (req, res) => { return; } + // Log download + run('UPDATE uploads SET downloads = downloads + 1 WHERE id = ?', [row.id]).catch(() => undefined); + logEvent('download', null, { name: filename, original: row.original_name }).catch(() => undefined); + res.download(row.stored_path, row.original_name); });