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

4
.gitignore vendored
View File

@@ -7,6 +7,10 @@ client/dist/
dist/ dist/
docroot/dist/ docroot/dist/
# Temporäre Bilder (werden nach 6 Stunden automatisch gelöscht)
tmp/
uploads/
# Node.js # Node.js
node_modules/ node_modules/
npm-debug.log* npm-debug.log*

View File

@@ -95,21 +95,33 @@ async function handleImageUpload(event) {
} }
try { try {
// Lese Bild als Base64 // Erstelle FormData für Upload
const reader = new FileReader(); const formData = new FormData();
reader.onload = (e) => { formData.append('image', file);
const base64Image = e.target.result;
// Sende Bild als Nachricht // Lade Bild hoch
chatStore.sendImage(chatStore.currentConversation, base64Image, file.type); const response = await fetch('/api/upload-image', {
}; method: 'POST',
reader.onerror = (error) => { body: formData,
console.error('Fehler beim Lesen des Bildes:', error); credentials: 'include' // Wichtig für Session-Cookies
alert('Fehler beim Lesen des Bildes'); });
};
reader.readAsDataURL(file); if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }));
throw new Error(errorData.error || 'Fehler beim Hochladen des Bildes');
}
const data = await response.json();
if (data.success && data.code) {
// Sende nur den Code über Socket.IO
chatStore.sendImage(chatStore.currentConversation, data.code, data.url);
} else {
throw new Error('Ungültige Antwort vom Server');
}
} catch (error) { } catch (error) {
console.error('Fehler beim Bild-Upload:', error); console.error('Fehler beim Bild-Upload:', error);
alert('Fehler beim Bild-Upload'); alert('Fehler beim Bild-Upload: ' + error.message);
} }
// Input zurücksetzen, damit das gleiche Bild erneut ausgewählt werden kann // Input zurücksetzen, damit das gleiche Bild erneut ausgewählt werden kann

View File

@@ -226,17 +226,19 @@ export const useChatStore = defineStore('chat', () => {
case 'message': case 'message':
// Debug-Logging für empfangene Nachrichten // Debug-Logging für empfangene Nachrichten
if (data.isImage) { if (data.isImage) {
console.log('[Bild empfangen] Von:', data.from, 'Typ:', data.imageType, 'Größe:', data.message ? data.message.length : 0, 'Zeichen'); console.log('[Bild empfangen] Von:', data.from, 'URL:', data.imageUrl || data.message);
} }
if (currentConversation.value === data.from) { if (currentConversation.value === data.from) {
const newMessage = { const newMessage = {
from: data.from, from: data.from,
message: data.message, message: data.imageUrl || data.message, // Verwende URL für Bilder
timestamp: data.timestamp, timestamp: data.timestamp,
self: false, self: false,
isImage: data.isImage || false, isImage: data.isImage || false,
imageType: data.imageType || null imageType: data.imageType || null,
imageUrl: data.imageUrl || null,
imageCode: data.imageCode || null
}; };
console.log('[Nachricht hinzugefügt]', newMessage); console.log('[Nachricht hinzugefügt]', newMessage);
@@ -254,11 +256,13 @@ export const useChatStore = defineStore('chat', () => {
currentConversation.value = data.with; currentConversation.value = data.with;
messages.value = data.messages.map(msg => ({ messages.value = data.messages.map(msg => ({
from: msg.from, from: msg.from,
message: msg.message, message: msg.imageUrl || msg.message, // Verwende URL für Bilder
timestamp: msg.timestamp, timestamp: msg.timestamp,
self: msg.from === userName.value, self: msg.from === userName.value,
isImage: msg.isImage || false, isImage: msg.isImage || false,
imageType: msg.imageType || null imageType: msg.imageType || null,
imageUrl: msg.imageUrl || null,
imageCode: msg.imageCode || null
})); }));
break; break;
case 'searchResults': case 'searchResults':
@@ -355,7 +359,7 @@ export const useChatStore = defineStore('chat', () => {
resetTimeoutTimer(); resetTimeoutTimer();
} }
function sendImage(toUserName, imageData, imageType) { function sendImage(toUserName, imageCode, imageUrl) {
if (!socket.value || !socket.value.connected) { if (!socket.value || !socket.value.connected) {
console.error('Socket.IO nicht verbunden'); console.error('Socket.IO nicht verbunden');
return; return;
@@ -369,20 +373,20 @@ export const useChatStore = defineStore('chat', () => {
const messageId = Date.now().toString(); const messageId = Date.now().toString();
socket.value.emit('message', { socket.value.emit('message', {
toUserName, toUserName,
message: imageData, // Base64-kodiertes Bild message: imageCode, // Nur der Code, nicht das gesamte Bild
messageId, messageId,
isImage: true, isImage: true,
imageType: imageType imageUrl: imageUrl // URL für das Bild
}); });
// Lokal hinzufügen // Lokal hinzufügen (mit URL)
messages.value.push({ messages.value.push({
from: userName.value, from: userName.value,
message: imageData, message: imageUrl, // Verwende URL statt Code für lokale Anzeige
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
self: true, self: true,
isImage: true, isImage: true,
imageType: imageType imageCode: imageCode
}); });
// Timeout zurücksetzen bei Aktivität // Timeout zurücksetzen bei Aktivität

View File

@@ -249,17 +249,23 @@ export function setupBroadcast(io) {
socket.emit('connected', connectedData); socket.emit('connected', connectedData);
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
console.log(`[Disconnect] Socket getrennt für Session-ID: ${sessionId}, Grund: ${reason}`);
const client = clients.get(sessionId); const client = clients.get(sessionId);
if (client) { if (client) {
console.log(`[Disconnect] Client gefunden: ${client.userName || 'unbekannt'}, Socket war verbunden: ${client.socket ? client.socket.connected : 'null'}`);
// Setze Socket auf null, damit keine Nachrichten mehr an diesen Client gesendet werden // Setze Socket auf null, damit keine Nachrichten mehr an diesen Client gesendet werden
// ABER: Lösche den Client NICHT, damit die Session beim Reload wiederhergestellt werden kann // ABER: Lösche den Client NICHT, damit die Session beim Reload wiederhergestellt werden kann
client.socket = null; client.socket = null;
// Aktualisiere Benutzerliste, damit andere Clients sehen, dass dieser Benutzer offline ist // Aktualisiere Benutzerliste, damit andere Clients sehen, dass dieser Benutzer offline ist
if (client.userName) { if (client.userName) {
console.log(`[Disconnect] Aktualisiere Benutzerliste nach Disconnect von ${client.userName}`);
broadcastUserList(); broadcastUserList();
} }
// Client bleibt in der Map, damit Session-Wiederherstellung funktioniert // Client bleibt in der Map, damit Session-Wiederherstellung funktioniert
} else {
console.log(`[Disconnect] Kein Client gefunden für Session-ID: ${sessionId}`);
} }
}); });
@@ -569,7 +575,7 @@ export function setupBroadcast(io) {
return; return;
} }
const { toUserName, message, messageId, isImage, imageType } = data; const { toUserName, message, messageId, isImage, imageType, imageUrl } = data;
if (!toUserName) { if (!toUserName) {
socket.emit('error', { message: 'Empfänger fehlt' }); socket.emit('error', { message: 'Empfänger fehlt' });
@@ -615,36 +621,63 @@ export function setupBroadcast(io) {
conversation.push({ conversation.push({
from: client.userName, from: client.userName,
to: toUserName, to: toUserName,
message, message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder
messageId, messageId,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
read: false, read: false,
isImage: isImage || false, isImage: isImage || false,
imageType: imageType || null imageType: imageType || null,
imageUrl: imageUrl || null,
imageCode: isImage ? message : null
}); });
// Sende an Empfänger (wenn online) // Sende an Empfänger (wenn online)
const messagePayload = { const messagePayload = {
from: client.userName, from: client.userName,
message, message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder
messageId, messageId,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
isImage: isImage || false, isImage: isImage || false,
imageType: imageType || null imageType: imageType || null,
imageUrl: imageUrl || null,
imageCode: isImage ? message : null // Code für Server-Referenz
}; };
// Debug-Logging für Bilder // Debug-Logging für Bilder
if (isImage) { if (isImage) {
console.log(`[Bild] Sende Bild von ${client.userName} an ${toUserName}, Größe: ${message ? message.length : 0} Zeichen, Typ: ${imageType || 'unbekannt'}`); console.log(`[Bild] Sende Bild von ${client.userName} an ${toUserName}, Größe: ${message ? message.length : 0} Zeichen, Typ: ${imageType || 'unbekannt'}`);
console.log(`[Bild] Absender Socket verbunden: ${socket.connected}, Empfänger Socket verbunden: ${receiver.socket ? receiver.socket.connected : 'null'}`);
} }
receiver.socket.emit('message', messagePayload); // Prüfe, ob Absender noch verbunden ist
if (!socket.connected) {
console.error(`[Bild] Absender ${client.userName} Socket nicht mehr verbunden beim Senden!`);
return;
}
// Bestätigung an Absender // Prüfe, ob Empfänger-Socket noch existiert und verbunden ist
socket.emit('messageSent', { if (!receiver.socket || !receiver.socket.connected) {
messageId, console.error(`[Bild] Empfänger ${toUserName} Socket nicht mehr verbunden beim Senden!`);
to: toUserName socket.emit('error', { message: 'Empfänger ist nicht mehr online' });
}); return;
}
try {
receiver.socket.emit('message', messagePayload);
// Bestätigung an Absender (nur wenn noch verbunden)
if (socket.connected) {
socket.emit('messageSent', {
messageId,
to: toUserName
});
}
} catch (error) {
console.error(`[Bild] Fehler beim Senden der Nachricht:`, error);
if (socket.connected) {
socket.emit('error', { message: 'Fehler beim Senden der Nachricht' });
}
}
// Aktualisiere ungelesene Nachrichten für den Empfänger // Aktualisiere ungelesene Nachrichten für den Empfänger
updateUnreadCount(receiver); updateUnreadCount(receiver);

View File

@@ -1,15 +1,171 @@
import { readFileSync } from 'fs'; import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { parse } from 'csv-parse/sync'; import { parse } from 'csv-parse/sync';
import multer from 'multer';
import crypto from 'crypto';
import axios from 'axios'; import axios from 'axios';
import { getSessionStatus, getClientsMap, getSessionIdForSocket } from './broadcast.js'; 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) { export function setupRoutes(app, __dirname) {
// Health-Check-Endpoint // Health-Check-Endpoint
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() }); 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 // Session-Status-Endpoint
app.get('/api/session', (req, res) => { app.get('/api/session', (req, res) => {