diff --git a/docker-compose.yml b/docker-compose.yml index c2b5e30..4c0a81b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,7 +79,7 @@ services: labels: - "traefik.enable=true" - - "traefik.http.routers.express.rule=Host(`${SERVICE_FQDN}`) && PathPrefix(`/manage`)" + - "traefik.http.routers.express.rule=Host(`${SERVICE_FQDN}`) && (PathPrefix(`/manage`) || PathPrefix(`/_share`))" - "traefik.http.routers.express.entrypoints=websecure" - "traefik.http.routers.express.tls=true" - "traefik.http.routers.express.tls.certresolver=letsencrypt" @@ -87,7 +87,7 @@ services: - "traefik.http.services.express-svc.loadbalancer.server.port=3000" - "traefik.http.routers.express.priority=10" # Optional HTTP redirect - - "traefik.http.routers.express-http.rule=Host(`${SERVICE_FQDN}`) && PathPrefix(`/manage`)" + - "traefik.http.routers.express-http.rule=Host(`${SERVICE_FQDN}`) && (PathPrefix(`/manage`) || PathPrefix(`/_share`))" - "traefik.http.routers.express-http.entrypoints=web" - "traefik.http.routers.express-http.middlewares=express-https-redirect" - "traefik.http.middlewares.express-https-redirect.redirectscheme.scheme=https" diff --git a/expressjs/data/uploads.sqlite b/expressjs/data/uploads.sqlite new file mode 100644 index 0000000..0db78db Binary files /dev/null and b/expressjs/data/uploads.sqlite differ diff --git a/expressjs/src/server.js b/expressjs/src/server.js index 506b292..fe01e1e 100644 --- a/expressjs/src/server.js +++ b/expressjs/src/server.js @@ -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) { ${title} @@ -375,29 +481,112 @@ function renderPage(title, body, mainClass = '') { ${title} @@ -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', '

Seite nicht gefunden.

')); }); +// 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}`); }); diff --git a/webserver.Dockerfile b/webserver.Dockerfile index 0ce3dc7..810b721 100644 --- a/webserver.Dockerfile +++ b/webserver.Dockerfile @@ -32,7 +32,13 @@ IndexOptions FancyIndexing FoldersFirst NameWidth=*\n\ # Force download for shared files RUN printf '\n# --- Force download in _share ---\n\ \n\ - Header set Content-Disposition "attachment"\n\ + # Header set Content-Disposition "attachment"\n\ + # We want to let the application handle the download name if possible\n\ + # or just serve it directly if accessed directly. \n\ + # Actually, we should probably proxy requests for metadata resolution\n\ + # But for now, let''s just keep it simple. \n\ + # If we want Apache to serve it with original name, we need a map.\n\ + # Since we moved logic to Node, Apache just serves raw files if needed.\n\ \n' >> /usr/local/apache2/conf/httpd.conf EXPOSE 80