import fsp from 'node:fs/promises'; import path from 'node:path'; import crypto from 'node:crypto'; import { joinRel, normalizeRelPath, safeJoin, sessionRoot } from './paths.mjs'; import { setFileMeta } from './fs-tree.mjs'; const sessions = new Map(); function sessionFile(ctx, id) { return path.join(sessionRoot(ctx), `${id}.part`); } function sessionMetaFile(ctx, id) { return path.join(sessionRoot(ctx), `${id}.json`); } export async function ensureSessionDir(ctx) { await fsp.mkdir(sessionRoot(ctx), { recursive: true }); } export async function createUploadSession(ctx, { relPath, totalSize, prefix = '', clientMeta = {} }) { await ensureSessionDir(ctx); const id = crypto.randomBytes(8).toString('hex'); const rel = joinRel(prefix, normalizeRelPath(relPath)); const meta = { id, rel, totalSize: Number(totalSize), received: 0, clientMeta, createdAt: Date.now(), userId: ctx.userId, }; await fsp.writeFile(sessionMetaFile(ctx, id), JSON.stringify(meta)); await fsp.writeFile(sessionFile(ctx, id), Buffer.alloc(0)); sessions.set(`${ctx.userId}:${id}`, meta); return meta; } async function loadSession(ctx, id) { const key = `${ctx.userId}:${id}`; if (sessions.has(key)) return sessions.get(key); const raw = await fsp.readFile(sessionMetaFile(ctx, id), 'utf8'); const meta = JSON.parse(raw); if (meta.userId && meta.userId !== ctx.userId) { throw new Error('session not found'); } sessions.set(key, meta); return meta; } export async function appendUploadChunk(ctx, id, buffer, offset) { const meta = await loadSession(ctx, id); const file = sessionFile(ctx, id); const handle = await fsp.open(file, 'r+'); try { await handle.write(buffer, 0, buffer.length, Number(offset)); } finally { await handle.close(); } meta.received = Math.max(meta.received, Number(offset) + buffer.length); await fsp.writeFile(sessionMetaFile(ctx, id), JSON.stringify(meta)); return meta; } export async function completeUploadSession(ctx, id) { const meta = await loadSession(ctx, id); const part = sessionFile(ctx, id); const stat = await fsp.stat(part); if (stat.size < meta.totalSize) throw new Error('incomplete'); const dest = safeJoin(ctx.uploadDir, meta.rel); await fsp.mkdir(path.dirname(dest), { recursive: true }); await fsp.rename(part, dest); await fsp.unlink(sessionMetaFile(ctx, id)).catch(() => {}); sessions.delete(`${ctx.userId}:${id}`); if (meta.clientMeta?.clientId) { await setFileMeta(ctx, meta.rel, meta.clientMeta); } return { saved: meta.rel, size: stat.size }; } export async function getSession(ctx, id) { return loadSession(ctx, id); }