Implement image upload functionality with temporary storage and cleanup. Update chat and broadcast logic to handle image URLs instead of base64 data, enhancing performance and user experience. Modify .gitignore to exclude temporary files and uploads directory.

This commit is contained in:
Torsten Schulz (local)
2025-12-05 10:43:27 +01:00
parent 840504e440
commit 6d922fbf9f
5 changed files with 245 additions and 36 deletions

View File

@@ -1,15 +1,171 @@
import { readFileSync } from 'fs';
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { parse } from 'csv-parse/sync';
import multer from 'multer';
import crypto from 'crypto';
import axios from 'axios';
import { getSessionStatus, getClientsMap, getSessionIdForSocket } from './broadcast.js';
// Bild-Upload-Konfiguration (temporäres Verzeichnis)
const uploadsDir = join(__dirname, '../tmp');
if (!existsSync(uploadsDir)) {
mkdirSync(uploadsDir, { recursive: true });
}
// Map: Code -> Bild-Info (für temporäre Speicherung)
const imageCodes = new Map();
// Multer-Konfiguration für Bild-Upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadsDir);
},
filename: (req, file, cb) => {
// Generiere eindeutigen Code
const code = crypto.randomBytes(16).toString('hex');
const ext = file.originalname.split('.').pop();
cb(null, `${code}.${ext}`);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB Limit
},
fileFilter: (req, file, cb) => {
// Nur Bilder erlauben
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Nur Bilder sind erlaubt'), false);
}
}
});
export function setupRoutes(app, __dirname) {
// Health-Check-Endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Bild-Upload-Endpoint
app.post('/api/upload-image', upload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'Kein Bild hochgeladen' });
}
// Prüfe, ob Benutzer eingeloggt ist
const sessionId = req.sessionID;
const clientsMap = getClientsMap();
const client = clientsMap.get(sessionId);
if (!client || !client.userName) {
// Lösche hochgeladenes Bild, wenn nicht eingeloggt
unlinkSync(req.file.path);
return res.status(401).json({ error: 'Nicht eingeloggt' });
}
// Generiere eindeutigen Code für das Bild
const code = req.file.filename.split('.')[0];
const imageInfo = {
code: code,
filename: req.file.filename,
originalName: req.file.originalname,
mimetype: req.file.mimetype,
size: req.file.size,
uploadedBy: client.userName,
uploadedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 6 * 60 * 60 * 1000) // 6 Stunden
};
// Speichere Bild-Info
imageCodes.set(code, imageInfo);
console.log(`[Bild-Upload] Bild hochgeladen von ${client.userName}, Code: ${code}`);
res.json({
success: true,
code: code,
url: `/api/image/${code}`
});
} catch (error) {
console.error('Fehler beim Bild-Upload:', error);
if (req.file && existsSync(req.file.path)) {
unlinkSync(req.file.path);
}
res.status(500).json({ error: 'Fehler beim Hochladen des Bildes' });
}
});
// Bild-Download-Endpoint (mit Code)
app.get('/api/image/:code', (req, res) => {
try {
const { code } = req.params;
const imageInfo = imageCodes.get(code);
if (!imageInfo) {
return res.status(404).json({ error: 'Bild nicht gefunden' });
}
// Prüfe Ablaufzeit
if (new Date() > imageInfo.expiresAt) {
// Lösche abgelaufenes Bild
const filePath = join(uploadsDir, imageInfo.filename);
if (existsSync(filePath)) {
unlinkSync(filePath);
}
imageCodes.delete(code);
return res.status(410).json({ error: 'Bild abgelaufen' });
}
// Prüfe, ob Benutzer eingeloggt ist (optional, für zusätzliche Sicherheit)
const sessionId = req.sessionID;
const clientsMap = getClientsMap();
const client = clientsMap.get(sessionId);
// Wenn eingeloggt, prüfe ob Benutzer berechtigt ist (optional)
// Für jetzt erlauben wir allen eingeloggten Benutzern den Zugriff
const filePath = join(uploadsDir, imageInfo.filename);
if (!existsSync(filePath)) {
imageCodes.delete(code);
return res.status(404).json({ error: 'Bilddatei nicht gefunden' });
}
// Setze Content-Type
res.setHeader('Content-Type', imageInfo.mimetype);
res.setHeader('Content-Disposition', `inline; filename="${imageInfo.originalName}"`);
// Sende Bild
const imageData = readFileSync(filePath);
res.send(imageData);
} catch (error) {
console.error('Fehler beim Laden des Bildes:', error);
res.status(500).json({ error: 'Fehler beim Laden des Bildes' });
}
});
// Cleanup-Funktion für abgelaufene Bilder (wird regelmäßig aufgerufen)
setInterval(() => {
const now = new Date();
let deletedCount = 0;
for (const [code, info] of imageCodes.entries()) {
if (now > info.expiresAt) {
const filePath = join(uploadsDir, info.filename);
if (existsSync(filePath)) {
unlinkSync(filePath);
}
imageCodes.delete(code);
deletedCount++;
}
}
if (deletedCount > 0) {
console.log(`[Bild-Cleanup] ${deletedCount} abgelaufene Bild(er) gelöscht`);
}
}, 30 * 60 * 1000); // Alle 30 Minuten prüfen
// Session-Status-Endpoint
app.get('/api/session', (req, res) => {