// ═══════════════════════════════════════════════════════════════════════════ // HF STORAGE SERVICE - HuggingFace CDN & Storage Integration // ═══════════════════════════════════════════════════════════════════════════ import { writeFile, readFile, mkdir, readdir, unlink } from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { StorageFile, SessionStorage, ChatMessage, Poll, Doubt } from './types'; // Data directory for persistent storage on HF Spaces const DATA_DIR = process.env.DATA_DIR || '/data'; const SESSIONS_DIR = path.join(DATA_DIR, 'sessions'); const UPLOADS_DIR = path.join(DATA_DIR, 'uploads'); // Ensure directories exist async function ensureDirectories() { if (!existsSync(DATA_DIR)) { await mkdir(DATA_DIR, { recursive: true }); } if (!existsSync(SESSIONS_DIR)) { await mkdir(SESSIONS_DIR, { recursive: true }); } if (!existsSync(UPLOADS_DIR)) { await mkdir(UPLOADS_DIR, { recursive: true }); } } // ═══════════════════════════════════════════════════════════════════════════ // FILE STORAGE (Local files served via HF CDN) // ═══════════════════════════════════════════════════════════════════════════ export class HFStorageService { private baseUrl: string; constructor() { // On HF Spaces, files are served from the Space's URL this.baseUrl = process.env.HF_SPACE_URL || ''; ensureDirectories(); } // ═══════════════════════════════════════════════════════════════════════════ // SESSION STORAGE // ═══════════════════════════════════════════════════════════════════════════ async saveSessionData(sessionId: string, data: any): Promise { const sessionDir = path.join(SESSIONS_DIR, sessionId); if (!existsSync(sessionDir)) { await mkdir(sessionDir, { recursive: true }); } const filePath = path.join(sessionDir, 'session.json'); await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); } async loadSessionData(sessionId: string): Promise { const filePath = path.join(SESSIONS_DIR, sessionId, 'session.json'); if (!existsSync(filePath)) { return null; } const data = await readFile(filePath, 'utf-8'); return JSON.parse(data); } async deleteSession(sessionId: string): Promise { const sessionDir = path.join(SESSIONS_DIR, sessionId); if (existsSync(sessionDir)) { const files = await readdir(sessionDir); for (const file of files) { await unlink(path.join(sessionDir, file)); } await unlink(sessionDir); } } // ═══════════════════════════════════════════════════════════════════════════ // FILE UPLOAD (Base64 images, slides, etc.) // ═══════════════════════════════════════════════════════════════════════════ async uploadFile( sessionId: string, fileName: string, base64Data: string, contentType: string = 'image/png' ): Promise { const sessionDir = path.join(SESSIONS_DIR, sessionId, 'files'); if (!existsSync(sessionDir)) { await mkdir(sessionDir, { recursive: true }); } // Extract base64 data const base64Content = base64Data.replace(/^data:[^;]+;base64,/, ''); const buffer = Buffer.from(base64Content, 'base64'); const filePath = path.join(sessionDir, fileName); await writeFile(filePath, buffer); return { path: filePath, url: `/api/storage/${sessionId}/${fileName}`, size: buffer.length, contentType, uploadedAt: new Date().toISOString() }; } async getFile(sessionId: string, fileName: string): Promise { const filePath = path.join(SESSIONS_DIR, sessionId, 'files', fileName); if (!existsSync(filePath)) { return null; } return readFile(filePath); } // ═══════════════════════════════════════════════════════════════════════════ // CHAT HISTORY // ═══════════════════════════════════════════════════════════════════════════ async saveChatMessage(sessionId: string, message: ChatMessage): Promise { const sessionDir = path.join(SESSIONS_DIR, sessionId); if (!existsSync(sessionDir)) { await mkdir(sessionDir, { recursive: true }); } const chatFile = path.join(sessionDir, 'chat.json'); let messages: ChatMessage[] = []; if (existsSync(chatFile)) { const data = await readFile(chatFile, 'utf-8'); messages = JSON.parse(data); } messages.push(message); await writeFile(chatFile, JSON.stringify(messages, null, 2), 'utf-8'); } async getChatHistory(sessionId: string): Promise { const chatFile = path.join(SESSIONS_DIR, sessionId, 'chat.json'); if (!existsSync(chatFile)) { return []; } const data = await readFile(chatFile, 'utf-8'); return JSON.parse(data); } // ═══════════════════════════════════════════════════════════════════════════ // POLL HISTORY // ═══════════════════════════════════════════════════════════════════════════ async savePoll(sessionId: string, poll: Poll): Promise { const sessionDir = path.join(SESSIONS_DIR, sessionId); if (!existsSync(sessionDir)) { await mkdir(sessionDir, { recursive: true }); } const pollsFile = path.join(sessionDir, 'polls.json'); let polls: Poll[] = []; if (existsSync(pollsFile)) { const data = await readFile(pollsFile, 'utf-8'); polls = JSON.parse(data); } const existingIndex = polls.findIndex(p => p.id === poll.id); if (existingIndex >= 0) { polls[existingIndex] = poll; } else { polls.push(poll); } await writeFile(pollsFile, JSON.stringify(polls, null, 2), 'utf-8'); } async getPolls(sessionId: string): Promise { const pollsFile = path.join(SESSIONS_DIR, sessionId, 'polls.json'); if (!existsSync(pollsFile)) { return []; } const data = await readFile(pollsFile, 'utf-8'); return JSON.parse(data); } // ═══════════════════════════════════════════════════════════════════════════ // DOUBTS // ═══════════════════════════════════════════════════════════════════════════ async saveDoubt(sessionId: string, doubt: Doubt): Promise { const sessionDir = path.join(SESSIONS_DIR, sessionId); if (!existsSync(sessionDir)) { await mkdir(sessionDir, { recursive: true }); } const doubtsFile = path.join(sessionDir, 'doubts.json'); let doubts: Doubt[] = []; if (existsSync(doubtsFile)) { const data = await readFile(doubtsFile, 'utf-8'); doubts = JSON.parse(data); } const existingIndex = doubts.findIndex(d => d.id === doubt.id); if (existingIndex >= 0) { doubts[existingIndex] = doubt; } else { doubts.push(doubt); } await writeFile(doubtsFile, JSON.stringify(doubts, null, 2), 'utf-8'); } async getDoubts(sessionId: string): Promise { const doubtsFile = path.join(SESSIONS_DIR, sessionId, 'doubts.json'); if (!existsSync(doubtsFile)) { return []; } const data = await readFile(doubtsFile, 'utf-8'); return JSON.parse(data); } // ═══════════════════════════════════════════════════════════════════════════ // WHITEBOARD SNAPSHOTS // ═══════════════════════════════════════════════════════════════════════════ async saveWhiteboardSnapshot( sessionId: string, slideNumber: number, base64Data: string ): Promise { const fileName = `whiteboard_slide_${slideNumber}_${Date.now()}.png`; return this.uploadFile(sessionId, fileName, base64Data, 'image/png'); } // ═══════════════════════════════════════════════════════════════════════════ // SESSION BACKUP (Full session export) // ═══════════════════════════════════════════════════════════════════════════ async exportSession(sessionId: string): Promise { const sessionData = await this.loadSessionData(sessionId); const chatHistory = await this.getChatHistory(sessionId); const polls = await this.getPolls(sessionId); const doubts = await this.getDoubts(sessionId); const filesDir = path.join(SESSIONS_DIR, sessionId, 'files'); const slides: StorageFile[] = []; const whiteboardSnapshots: StorageFile[] = []; if (existsSync(filesDir)) { const files = await readdir(filesDir); for (const file of files) { if (file.startsWith('slide_')) { slides.push({ path: path.join(filesDir, file), url: `/api/storage/${sessionId}/${file}`, size: 0, contentType: 'image/png', uploadedAt: new Date().toISOString() }); } else if (file.startsWith('whiteboard_')) { whiteboardSnapshots.push({ path: path.join(filesDir, file), url: `/api/storage/${sessionId}/${file}`, size: 0, contentType: 'image/png', uploadedAt: new Date().toISOString() }); } } } return { sessionId, slides, whiteboardSnapshots, doubts: doubts.map(d => ({ path: '', url: '', size: 0, contentType: 'application/json', uploadedAt: d.createdAt })), chatHistory, pollHistory: polls }; } } // Singleton instance export const storageService = new HFStorageService();