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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,6 +7,10 @@ client/dist/
|
||||
dist/
|
||||
docroot/dist/
|
||||
|
||||
# Temporäre Bilder (werden nach 6 Stunden automatisch gelöscht)
|
||||
tmp/
|
||||
uploads/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
@@ -95,21 +95,33 @@ async function handleImageUpload(event) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Lese Bild als Base64
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64Image = e.target.result;
|
||||
// Sende Bild als Nachricht
|
||||
chatStore.sendImage(chatStore.currentConversation, base64Image, file.type);
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
console.error('Fehler beim Lesen des Bildes:', error);
|
||||
alert('Fehler beim Lesen des Bildes');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
// Erstelle FormData für Upload
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
// Lade Bild hoch
|
||||
const response = await fetch('/api/upload-image', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include' // Wichtig für Session-Cookies
|
||||
});
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -226,17 +226,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'message':
|
||||
// Debug-Logging für empfangene Nachrichten
|
||||
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) {
|
||||
const newMessage = {
|
||||
from: data.from,
|
||||
message: data.message,
|
||||
message: data.imageUrl || data.message, // Verwende URL für Bilder
|
||||
timestamp: data.timestamp,
|
||||
self: 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);
|
||||
@@ -254,11 +256,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
currentConversation.value = data.with;
|
||||
messages.value = data.messages.map(msg => ({
|
||||
from: msg.from,
|
||||
message: msg.message,
|
||||
message: msg.imageUrl || msg.message, // Verwende URL für Bilder
|
||||
timestamp: msg.timestamp,
|
||||
self: msg.from === userName.value,
|
||||
isImage: msg.isImage || false,
|
||||
imageType: msg.imageType || null
|
||||
imageType: msg.imageType || null,
|
||||
imageUrl: msg.imageUrl || null,
|
||||
imageCode: msg.imageCode || null
|
||||
}));
|
||||
break;
|
||||
case 'searchResults':
|
||||
@@ -355,7 +359,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
resetTimeoutTimer();
|
||||
}
|
||||
|
||||
function sendImage(toUserName, imageData, imageType) {
|
||||
function sendImage(toUserName, imageCode, imageUrl) {
|
||||
if (!socket.value || !socket.value.connected) {
|
||||
console.error('Socket.IO nicht verbunden');
|
||||
return;
|
||||
@@ -369,20 +373,20 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const messageId = Date.now().toString();
|
||||
socket.value.emit('message', {
|
||||
toUserName,
|
||||
message: imageData, // Base64-kodiertes Bild
|
||||
message: imageCode, // Nur der Code, nicht das gesamte Bild
|
||||
messageId,
|
||||
isImage: true,
|
||||
imageType: imageType
|
||||
imageUrl: imageUrl // URL für das Bild
|
||||
});
|
||||
|
||||
// Lokal hinzufügen
|
||||
// Lokal hinzufügen (mit URL)
|
||||
messages.value.push({
|
||||
from: userName.value,
|
||||
message: imageData,
|
||||
message: imageUrl, // Verwende URL statt Code für lokale Anzeige
|
||||
timestamp: new Date().toISOString(),
|
||||
self: true,
|
||||
isImage: true,
|
||||
imageType: imageType
|
||||
imageCode: imageCode
|
||||
});
|
||||
|
||||
// Timeout zurücksetzen bei Aktivität
|
||||
|
||||
@@ -249,17 +249,23 @@ export function setupBroadcast(io) {
|
||||
socket.emit('connected', connectedData);
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`[Disconnect] Socket getrennt für Session-ID: ${sessionId}, Grund: ${reason}`);
|
||||
const client = clients.get(sessionId);
|
||||
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
|
||||
// ABER: Lösche den Client NICHT, damit die Session beim Reload wiederhergestellt werden kann
|
||||
client.socket = null;
|
||||
|
||||
// Aktualisiere Benutzerliste, damit andere Clients sehen, dass dieser Benutzer offline ist
|
||||
if (client.userName) {
|
||||
console.log(`[Disconnect] Aktualisiere Benutzerliste nach Disconnect von ${client.userName}`);
|
||||
broadcastUserList();
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
const { toUserName, message, messageId, isImage, imageType } = data;
|
||||
const { toUserName, message, messageId, isImage, imageType, imageUrl } = data;
|
||||
|
||||
if (!toUserName) {
|
||||
socket.emit('error', { message: 'Empfänger fehlt' });
|
||||
@@ -615,36 +621,63 @@ export function setupBroadcast(io) {
|
||||
conversation.push({
|
||||
from: client.userName,
|
||||
to: toUserName,
|
||||
message,
|
||||
message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder
|
||||
messageId,
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
isImage: isImage || false,
|
||||
imageType: imageType || null
|
||||
imageType: imageType || null,
|
||||
imageUrl: imageUrl || null,
|
||||
imageCode: isImage ? message : null
|
||||
});
|
||||
|
||||
// Sende an Empfänger (wenn online)
|
||||
const messagePayload = {
|
||||
from: client.userName,
|
||||
message,
|
||||
message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder
|
||||
messageId,
|
||||
timestamp: new Date().toISOString(),
|
||||
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
|
||||
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] Absender Socket verbunden: ${socket.connected}, Empfänger Socket verbunden: ${receiver.socket ? receiver.socket.connected : 'null'}`);
|
||||
}
|
||||
|
||||
// Prüfe, ob Absender noch verbunden ist
|
||||
if (!socket.connected) {
|
||||
console.error(`[Bild] Absender ${client.userName} Socket nicht mehr verbunden beim Senden!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe, ob Empfänger-Socket noch existiert und verbunden ist
|
||||
if (!receiver.socket || !receiver.socket.connected) {
|
||||
console.error(`[Bild] Empfänger ${toUserName} Socket nicht mehr verbunden beim Senden!`);
|
||||
socket.emit('error', { message: 'Empfänger ist nicht mehr online' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
receiver.socket.emit('message', messagePayload);
|
||||
|
||||
// Bestätigung an Absender
|
||||
// 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
|
||||
updateUnreadCount(receiver);
|
||||
|
||||
158
server/routes.js
158
server/routes.js
@@ -1,16 +1,172 @@
|
||||
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) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user