slidemap / src /lib /storage.ts
Nanny7's picture
Initial commit: Education platform for HuggingFace Spaces
c5dfbfe
Raw
History Blame Contribute Delete
12.2 kB
// ═══════════════════════════════════════════════════════════════════════════
// 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<void> {
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<any | null> {
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<void> {
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<StorageFile> {
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<Buffer | null> {
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<void> {
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<ChatMessage[]> {
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<void> {
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<Poll[]> {
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<void> {
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<Doubt[]> {
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<StorageFile> {
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<SessionStorage> {
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();