// Bundle byte store — where published ZIPs physically live. // // File-backed today (`data/courseware-bundles//v.zip`); swap for // object storage + CDN behind the same interface when configured. The record's // `bundleUrl` is where the player downloads from — a CDN direct link once the // object store is wired. import { promises as fs } from 'fs'; import path from 'path'; import { assertCoursewareId, assertCoursewareVersion, resolveDirectChildPath } from './identity'; export const COURSEWARE_BUNDLES_DIR = process.env.COURSEWARE_BUNDLE_DIR ?? path.join(process.env.LEARNING_DATA_DIR ?? path.join(process.cwd(), 'data'), 'courseware-bundles'); export interface BundleByteStore { save(coursewareId: string, version: number, bytes: Uint8Array): Promise; read(coursewareId: string, version: number): Promise; remove(coursewareId: string, version: number): Promise; storageKey(coursewareId: string, version: number): string; } export function createFileBundleByteStore(dir: string = COURSEWARE_BUNDLES_DIR): BundleByteStore { const resolvePath = (coursewareId: string, version: number) => { assertCoursewareId(coursewareId); assertCoursewareVersion(version); const coursewareDir = resolveDirectChildPath(dir, coursewareId); return resolveDirectChildPath(coursewareDir, `v${version}.zip`); }; return { async save(coursewareId, version, bytes) { const filePath = resolvePath(coursewareId, version); await fs.mkdir(path.dirname(filePath), { recursive: true }); const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(tempPath, bytes); try { // link() is an atomic create-if-absent operation. A published version // can never be overwritten with different bytes. await fs.link(tempPath, filePath); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; const existing = await fs.readFile(filePath); if (!existing.equals(Buffer.from(bytes))) { throw new Error(`Bundle ${coursewareId} v${version} already exists and is immutable`); } } finally { await fs.rm(tempPath, { force: true }).catch(() => undefined); } return `${coursewareId}/v${version}.zip`; }, async read(coursewareId, version) { try { return await fs.readFile(resolvePath(coursewareId, version)); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error; } }, async remove(coursewareId, version) { await fs.rm(resolvePath(coursewareId, version), { force: true }); }, storageKey(coursewareId, version) { assertCoursewareId(coursewareId); assertCoursewareVersion(version); return `${coursewareId}/v${version}.zip`; }, }; }