UI rehaul + original download filenames
This commit is contained in:
BIN
expressjs/data/uploads.sqlite
Normal file
BIN
expressjs/data/uploads.sqlite
Normal file
Binary file not shown.
@@ -257,12 +257,11 @@ function createRandomId() {
|
||||
function sanitizeBaseName(originalName) {
|
||||
const ext = path.extname(originalName || '');
|
||||
const base = path.basename(originalName || 'datei', ext);
|
||||
// Allow more characters but keep it safe for URLs and FS
|
||||
const cleaned = base
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^[-_]+|[-_]+$/g, '');
|
||||
.replace(/[^\w\-. ]/g, '') // Allow words, dashes, dots, spaces
|
||||
.trim()
|
||||
.replace(/\s+/g, '-'); // Replace spaces with dashes for better URLs
|
||||
return cleaned || 'datei';
|
||||
}
|
||||
|
||||
@@ -318,45 +317,152 @@ function renderFileManagerPage(title, body) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
:root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; --accent-soft:#e6f4f2; }
|
||||
:root {
|
||||
--bg: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
--text-main: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--primary: #0f766e;
|
||||
--primary-hover: #0d9488;
|
||||
--primary-light: #f0fdfa;
|
||||
--danger: #ef4444;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; background: var(--bg); color: var(--ink); }
|
||||
main { max-width: 1280px; margin: 0 auto; padding: 26px 18px 70px; }
|
||||
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 18px 20px; border-radius: 16px; background: linear-gradient(135deg, #ffffff 0%, #f6fbfa 100%); border: 1px solid var(--line); }
|
||||
h1 { margin: 0; font-size: 1.75rem; letter-spacing: 0.01em; }
|
||||
h2 { margin: 0 0 12px; font-size: 1.08rem; }
|
||||
.muted { color: var(--muted); font-size: 0.95rem; }
|
||||
.card { margin-top: 18px; padding: 16px; background: var(--card); border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(12, 18, 28, 0.08); }
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.tag { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
|
||||
.tag.primary { background: var(--accent-soft); border-color: #bfe6e0; color: var(--accent-strong); }
|
||||
.tag span { color: var(--muted); font-weight: 500; }
|
||||
.browser-shell { display: grid; gap: 16px; }
|
||||
.browser-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.crumbs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||
.crumbs a { color: var(--accent-strong); text-decoration: none; font-weight: 600; }
|
||||
.crumbs span { color: var(--muted); }
|
||||
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
||||
form { display: grid; gap: 10px; }
|
||||
label { display: grid; gap: 6px; font-weight: 600; }
|
||||
input, button, select { font: inherit; padding: 9px 12px; border-radius: 10px; }
|
||||
input, select { border: 1px solid var(--line); background: #fff; }
|
||||
button { border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer; transition: transform 0.15s ease, background 0.2s ease; }
|
||||
button:hover { background: var(--accent-strong); }
|
||||
button.secondary { background: transparent; color: var(--accent); }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
||||
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
tbody tr:hover { background: #f2f8f7; }
|
||||
.name { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.name strong { font-weight: 700; }
|
||||
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.actions button { font-size: 0.9rem; padding: 7px 10px; }
|
||||
dialog { border: none; border-radius: 16px; padding: 0; width: min(520px, 92vw); }
|
||||
dialog::backdrop { background: rgba(15, 23, 42, 0.4); }
|
||||
.dialog-card { padding: 18px; display: grid; gap: 12px; }
|
||||
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.dialog-actions .secondary { border-color: var(--line); color: var(--accent-strong); }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
main { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; }
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
h1 { margin: 0; font-size: 1.5rem; font-weight: 700; color: var(--text-main); }
|
||||
h2 { margin: 0 0 1rem; font-size: 1.1rem; font-weight: 600; }
|
||||
.muted { color: var(--text-muted); font-size: 0.875rem; }
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-main);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tag:hover { border-color: var(--text-muted); }
|
||||
.tag.primary {
|
||||
background: var(--primary-light);
|
||||
border-color: #ccfbf1;
|
||||
color: var(--primary);
|
||||
}
|
||||
.tag.primary:hover { border-color: var(--primary); }
|
||||
.tag span { color: var(--text-muted); }
|
||||
|
||||
.browser-shell { display: grid; gap: 1.5rem; }
|
||||
.browser-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
|
||||
.crumbs { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; font-size: 0.95rem; }
|
||||
.crumbs a { color: var(--primary); text-decoration: none; font-weight: 500; }
|
||||
.crumbs a:hover { text-decoration: underline; }
|
||||
.crumbs span { color: var(--text-muted); }
|
||||
|
||||
.grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
||||
|
||||
form { display: grid; gap: 1rem; }
|
||||
label { display: grid; gap: 0.375rem; font-weight: 500; font-size: 0.9rem; }
|
||||
|
||||
input, button, select {
|
||||
font: inherit;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
}
|
||||
input, select {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px var(--primary-light);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid transparent;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
button:hover { background: var(--primary-hover); }
|
||||
button.secondary {
|
||||
background: white;
|
||||
border-color: var(--border);
|
||||
color: var(--text-main);
|
||||
}
|
||||
button.secondary:hover { border-color: var(--text-muted); background: var(--bg); }
|
||||
button.danger {
|
||||
background: #fee2e2;
|
||||
color: var(--danger);
|
||||
}
|
||||
button.danger:hover { background: #fecaca; }
|
||||
|
||||
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.95rem; }
|
||||
th { text-align: left; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); color: var(--text-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: var(--bg); }
|
||||
|
||||
.name { display: inline-flex; align-items: center; gap: 0.75rem; font-weight: 500; }
|
||||
.folder { color: var(--primary); text-decoration: none; }
|
||||
.folder:hover { text-decoration: underline; }
|
||||
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||
.actions button { font-size: 0.8rem; padding: 0.25rem 0.6rem; }
|
||||
|
||||
dialog {
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 0;
|
||||
width: min(480px, 95vw);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
dialog::backdrop { background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(2px); }
|
||||
.dialog-card { padding: 1.5rem; display: grid; gap: 1.25rem; }
|
||||
.dialog-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -375,29 +481,112 @@ function renderPage(title, body, mainClass = '') {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
:root { --ink:#0f172a; --muted:#6b7280; --line:#e5e7eb; --bg:#f7f7f4; --card:#ffffff; --accent:#111827; }
|
||||
:root {
|
||||
--bg: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
--text-main: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--primary: #0f172a; /* Darker primary for admin/user dashboard */
|
||||
--primary-hover: #334155;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; color: var(--ink); background: var(--bg); }
|
||||
main { max-width: 920px; margin: 0 auto; padding: 22px 16px 48px; }
|
||||
main.wide { max-width: 1180px; }
|
||||
header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
h1 { margin: 0; font-size: 1.55rem; }
|
||||
h2 { margin: 0 0 12px; font-size: 1.08rem; }
|
||||
.muted { color: var(--muted); font-size: 0.95rem; }
|
||||
.card { margin-top: 16px; padding: 16px; background: var(--card); border-radius: 12px; border: 1px solid var(--line); }
|
||||
form { display: grid; gap: 10px; }
|
||||
label { display: grid; gap: 6px; font-weight: 600; }
|
||||
input, button { font: inherit; padding: 8px 10px; border-radius: 8px; }
|
||||
input { border: 1px solid var(--line); background: #fff; }
|
||||
button { border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer; }
|
||||
button.secondary { background: transparent; color: var(--accent); }
|
||||
.row { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
|
||||
th, td { text-align: left; padding: 6px 4px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
progress { width: 100%; height: 12px; accent-color: var(--accent); }
|
||||
.actions { display: grid; gap: 6px; }
|
||||
.actions input, .actions button { font-size: 0.9rem; padding: 6px 8px; }
|
||||
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #f1f5f9; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text-main);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1rem; }
|
||||
main.wide { max-width: 1200px; }
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
h1 { margin: 0; font-size: 1.75rem; font-weight: 700; }
|
||||
h2 { margin: 0 0 1.25rem; font-size: 1.1rem; font-weight: 600; }
|
||||
.muted { color: var(--text-muted); font-size: 0.875rem; }
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
form { display: grid; gap: 1rem; }
|
||||
label { display: grid; gap: 0.375rem; font-weight: 500; font-size: 0.9rem; }
|
||||
|
||||
input, button {
|
||||
font: inherit;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
}
|
||||
input {
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px #e2e8f0;
|
||||
}
|
||||
button {
|
||||
border: 1px solid transparent;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover { background: var(--primary-hover); }
|
||||
button.secondary {
|
||||
background: white;
|
||||
border-color: var(--border);
|
||||
color: var(--text-main);
|
||||
}
|
||||
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; }
|
||||
|
||||
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; }
|
||||
td { padding: 0.75rem; border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: var(--bg); }
|
||||
|
||||
progress { width: 100%; height: 0.75rem; border-radius: 99px; overflow: hidden; }
|
||||
progress::-webkit-progress-bar { background-color: var(--border); }
|
||||
progress::-webkit-progress-value { background-color: var(--primary); }
|
||||
|
||||
.actions { display: flex; gap: 0.5rem; align-items: center; }
|
||||
.actions form { display: flex; gap: 0.5rem; }
|
||||
.actions input, .actions button { font-size: 0.8rem; padding: 0.35rem 0.6rem; }
|
||||
|
||||
.pill { display: inline-flex; padding: 0.25rem 0.75rem; border-radius: 999px; background: #fee2e2; color: #991b1b; font-size: 0.85rem; font-weight: 500; margin-bottom: 1rem; }
|
||||
a { color: inherit; text-decoration-color: var(--border); }
|
||||
a:hover { color: var(--primary); text-decoration-color: var(--primary); }
|
||||
|
||||
/* Toolbar helpers for admin */
|
||||
.toolbar { display: flex; gap: 0.75rem; }
|
||||
.tag { display: inline-flex; align-items: center; padding: 0.375rem 0.75rem; border-radius: 999px; background: #fff; border: 1px solid var(--border); text-decoration: none; font-size: 0.875rem; font-weight: 500; }
|
||||
.tag.primary { background: #f1f5f9; color: var(--text-main); }
|
||||
.tag:hover { border-color: var(--text-muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1555,10 +1744,12 @@ app.post(`${basePath}/api/upload`, requireAuthApi, upload.single('file'), async
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const ext = sanitizeExtension(req.file.originalname);
|
||||
const baseName = sanitizeBaseName(req.file.originalname);
|
||||
// Don't sanitize rigorously anymore, we rely on content-disposition header for safety
|
||||
// but we still want a safe filename for storage
|
||||
const ext = path.extname(req.file.originalname);
|
||||
const token = createRandomId();
|
||||
const storedName = `_${baseName}-${token}${ext}`;
|
||||
// We keep the original filename in the DB but store it with a safe ID on disk
|
||||
const storedName = `${token}${ext}`;
|
||||
const storedPath = path.join(shareDir, storedName);
|
||||
|
||||
const retentionOverride = parseFloat(req.body.retentionHours || '');
|
||||
@@ -1583,7 +1774,7 @@ app.post(`${basePath}/api/upload`, requireAuthApi, upload.single('file'), async
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
req.user.username,
|
||||
req.file.originalname,
|
||||
req.file.originalname, // Store exact original name
|
||||
storedName,
|
||||
storedPath,
|
||||
req.file.size,
|
||||
@@ -1643,6 +1834,32 @@ app.use((req, res) => {
|
||||
res.status(404).send(renderPage('Nicht gefunden', '<p class="card">Seite nicht gefunden.</p>'));
|
||||
});
|
||||
|
||||
// Add endpoint to serve files with correct headers
|
||||
// NOTE: This must be mounted at root level or matching the path prefix handled by Traefik
|
||||
app.get('/_share/:filename', async (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
// Security check: ensure no path traversal
|
||||
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
||||
res.status(400).send('Invalid filename');
|
||||
return;
|
||||
}
|
||||
|
||||
const row = await get('SELECT 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)) {
|
||||
res.download(filePath, filename); // Fallback: download with stored name
|
||||
return;
|
||||
}
|
||||
res.status(404).send('File not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.download(row.stored_path, row.original_name);
|
||||
});
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Express server listening on ${port} with base path ${basePath}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user