import crypto from 'crypto'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; export interface Session { id: string; key: string; createdAt: Date; lastAccessed: Date; metadata?: Record; } export class SessionManager { private static instance: SessionManager; private sessions: Map = new Map(); private sessionDir: string; private publicDir: string; private constructor() { // Use /data for Hugging Face persistent storage, fallback to local for development const baseDataDir = process.env.NODE_ENV === 'production' && fsSync.existsSync('/data') ? '/data' : path.join(process.cwd(), 'data'); this.sessionDir = path.join(baseDataDir, 'files'); this.publicDir = path.join(baseDataDir, 'public'); this.initializeDirectories(); } static getInstance(): SessionManager { if (!SessionManager.instance) { SessionManager.instance = new SessionManager(); } return SessionManager.instance; } private async initializeDirectories() { try { await fs.mkdir(this.sessionDir, { recursive: true }); await fs.mkdir(this.publicDir, { recursive: true }); } catch (error) { console.error('Error initializing directories:', error); } } generateSessionKey(): string { return crypto.randomBytes(32).toString('hex'); } async createSession(metadata?: Record): Promise { const sessionId = `session_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`; const sessionKey = this.generateSessionKey(); const session: Session = { id: sessionId, key: sessionKey, createdAt: new Date(), lastAccessed: new Date(), metadata }; this.sessions.set(sessionKey, session); // Create session directory const sessionPath = path.join(this.sessionDir, sessionId); await fs.mkdir(sessionPath, { recursive: true }); // Save session metadata await fs.writeFile( path.join(sessionPath, 'session.json'), JSON.stringify(session, null, 2) ); return session; } async validateSession(sessionIdOrKey: string): Promise { // First check if it's a session ID (starts with "session_") if (sessionIdOrKey.startsWith('session_')) { return this.validateSessionById(sessionIdOrKey); } // Otherwise treat as session key for backward compatibility if (this.sessions.has(sessionIdOrKey)) { const session = this.sessions.get(sessionIdOrKey)!; session.lastAccessed = new Date(); return true; } // Check if session exists on disk by key try { const sessionsOnDisk = await fs.readdir(this.sessionDir); for (const sessionId of sessionsOnDisk) { const sessionPath = path.join(this.sessionDir, sessionId, 'session.json'); try { const sessionData = await fs.readFile(sessionPath, 'utf-8'); const session = JSON.parse(sessionData) as Session; if (session.key === sessionIdOrKey) { session.lastAccessed = new Date(); this.sessions.set(sessionIdOrKey, session); return true; } } catch (error) { continue; } } } catch (error) { console.error('Error validating session:', error); } return false; } async validateSessionById(sessionId: string): Promise { // Check in memory first for (const [key, session] of this.sessions.entries()) { if (session.id === sessionId) { session.lastAccessed = new Date(); return true; } } // Check on disk try { const sessionPath = path.join(this.sessionDir, sessionId, 'session.json'); const sessionData = await fs.readFile(sessionPath, 'utf-8'); const session = JSON.parse(sessionData) as Session; // Load into memory session.lastAccessed = new Date(); this.sessions.set(session.key, session); return true; } catch (error) { console.log('Session not found:', sessionId); } return false; } async getSession(sessionIdOrKey: string): Promise { // If it's a session ID, find by ID if (sessionIdOrKey.startsWith('session_')) { for (const [key, session] of this.sessions.entries()) { if (session.id === sessionIdOrKey) { return session; } } // Try loading from disk try { const sessionPath = path.join(this.sessionDir, sessionIdOrKey, 'session.json'); const sessionData = await fs.readFile(sessionPath, 'utf-8'); const session = JSON.parse(sessionData) as Session; this.sessions.set(session.key, session); return session; } catch (error) { return null; } } // Otherwise treat as key if (await this.validateSession(sessionIdOrKey)) { return this.sessions.get(sessionIdOrKey) || null; } return null; } async getSessionPath(sessionIdOrKey: string): Promise { // If it's already a session ID, use it directly if (sessionIdOrKey.startsWith('session_')) { const sessionPath = path.join(this.sessionDir, sessionIdOrKey); try { await fs.access(sessionPath); return sessionPath; } catch { return null; } } // Otherwise get session by key const session = await this.getSession(sessionIdOrKey); if (session) { return path.join(this.sessionDir, session.id); } return null; } getPublicPath(): string { return this.publicDir; } async listSessionFiles(sessionIdOrKey: string): Promise { const sessionPath = await this.getSessionPath(sessionIdOrKey); if (!sessionPath) { throw new Error('Invalid session ID or key'); } try { const files = await fs.readdir(sessionPath); return files.filter(file => file !== 'session.json'); } catch (error) { console.error('Error listing session files:', error); return []; } } async saveFileToSession(sessionIdOrKey: string, fileName: string, content: Buffer | string): Promise { const sessionPath = await this.getSessionPath(sessionIdOrKey); if (!sessionPath) { throw new Error('Invalid session ID or key'); } const filePath = path.join(sessionPath, fileName); await fs.writeFile(filePath, content); return filePath; } async getFileFromSession(sessionIdOrKey: string, fileName: string): Promise { const sessionPath = await this.getSessionPath(sessionIdOrKey); if (!sessionPath) { throw new Error('Invalid session ID or key'); } const filePath = path.join(sessionPath, fileName); return await fs.readFile(filePath); } async saveFileToPublic(fileName: string, content: Buffer | string): Promise { const filePath = path.join(this.publicDir, fileName); await fs.writeFile(filePath, content); return filePath; } async getFileFromPublic(fileName: string): Promise { const filePath = path.join(this.publicDir, fileName); return await fs.readFile(filePath); } async deleteSession(sessionIdOrKey: string): Promise { const sessionPath = await this.getSessionPath(sessionIdOrKey); if (!sessionPath) { return false; } try { await fs.rm(sessionPath, { recursive: true, force: true }); // Remove from memory by finding the right key if (sessionIdOrKey.startsWith('session_')) { for (const [key, session] of this.sessions.entries()) { if (session.id === sessionIdOrKey) { this.sessions.delete(key); break; } } } else { this.sessions.delete(sessionIdOrKey); } return true; } catch (error) { console.error('Error deleting session:', error); return false; } } // Clean up old sessions (older than 24 hours) async cleanupOldSessions(): Promise { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); try { const sessionsOnDisk = await fs.readdir(this.sessionDir); for (const sessionId of sessionsOnDisk) { const sessionPath = path.join(this.sessionDir, sessionId, 'session.json'); try { const sessionData = await fs.readFile(sessionPath, 'utf-8'); const session = JSON.parse(sessionData) as Session; if (new Date(session.lastAccessed) < oneDayAgo) { await fs.rm(path.join(this.sessionDir, sessionId), { recursive: true, force: true }); } } catch (error) { continue; } } } catch (error) { console.error('Error cleaning up old sessions:', error); } } }