diff --git a/server.js b/server.js index f8a1c31..e8940c6 100644 --- a/server.js +++ b/server.js @@ -587,25 +587,51 @@ app.post("/api/presigned", requireAuth, async (req, res) => { // Sessions stored in memory; relay handles actual transfer const udpSessions = new Map(); -app.post("/api/udp/session", requireAuth, (req, res) => { +app.post("/api/udp/session", requireAuth, async (req, res) => { const { filename, size, prefix } = req.body; if (!filename || !size) return res.status(400).json({ success: false, error: "filename and size required" }); const relayUrl = db.relayConfig?.relayUrl || ""; const udpPort = db.relayConfig?.udpPort || 5000; if (!relayUrl) return res.status(503).json({ success: false, error: "UDP relay not configured. Go to Admin → Relay Settings." }); - // Public URL is what the browser connects to — falls back to internal if not set const publicRelayUrl = (db.relayConfig?.publicRelayUrl || "").trim() || relayUrl; const sessionId = crypto.randomBytes(16).toString("hex"); const key = (prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename); + const s3Cfg = db.s3Config || {}; + + // Register session on the relay server so it can accept chunks and upload to S3 + try { + const relayResp = await fetch(`${relayUrl}/session`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + sessionId, filename, size, key, + bucket: s3Cfg.bucket || "", + s3Config: { + endpoint: s3Cfg.endpoint || "", + region: s3Cfg.region || "us-east-1", + accessKeyId: s3Cfg.accessKeyId || "", + secretAccessKey: s3Cfg.secretAccessKey || "", + }, + }), + signal: AbortSignal.timeout(5000), + }); + if (!relayResp.ok) { + const err = await relayResp.json().catch(() => ({})); + return res.status(502).json({ success: false, error: `Relay error: ${err.error || relayResp.status}` }); + } + } catch (err) { + console.error("[UDP] Failed to register session on relay:", err.message); + return res.status(502).json({ success: false, error: `Cannot reach relay: ${err.message}` }); + } + udpSessions.set(sessionId, { sessionId, filename, size, key, status: "pending", createdAt: Date.now(), updatedAt: Date.now(), }); - // Cleanup after 2 hours setTimeout(() => udpSessions.delete(sessionId), 2 * 60 * 60 * 1000); - res.json({ success: true, sessionId, relayUrl: publicRelayUrl, udpPort, key, s3Bucket: db.s3Config?.bucket || "" }); + res.json({ success: true, sessionId, relayUrl: publicRelayUrl, udpPort, key, s3Bucket: s3Cfg.bucket || "" }); }); app.get("/api/udp/session/:id", requireAuth, (req, res) => { diff --git a/udp-relay/server.js b/udp-relay/server.js index 43b4938..dafecb6 100644 --- a/udp-relay/server.js +++ b/udp-relay/server.js @@ -235,7 +235,18 @@ udpServer.bind(UDP_PORT, "0.0.0.0", () => { // ==================== HTTP CONTROL API ==================== const app = express(); + +// CORS — browsers (Chrome extension) send chunks via HTTP fallback +app.use((req, res, next) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization"); + if (req.method === "OPTIONS") return res.sendStatus(204); + next(); +}); + app.use(express.json()); +app.use(express.raw({ type: "application/octet-stream", limit: "100mb" })); // Health check app.get("/health", (req, res) => { @@ -266,6 +277,38 @@ app.post("/session", (req, res) => { res.json({ success: true, sessionId, udpPort: UDP_PORT, chunkSize: CHUNK_SIZE, expectedChunks: session.expectedChunks }); }); +// HTTP chunk fallback — Chrome extensions can't send raw UDP, so they POST chunks over HTTP +app.post("/session/:id/chunk/:index", async (req, res) => { + const session = sessions.get(req.params.id); + if (!session) return res.status(404).json({ error: "Session not found" }); + const chunkIndex = parseInt(req.params.index); + if (isNaN(chunkIndex)) return res.status(400).json({ error: "Invalid chunk index" }); + + try { + // Initialize multipart upload on first chunk + if (session.status === "pending") { + await session.initMultipart(); + } + await session.receiveChunk(chunkIndex, req.body); + + // Auto-complete when all chunks received + if (session.chunks.size === session.expectedChunks && session.status === "receiving") { + await session.complete(); + } + + res.json({ + success: true, + chunkIndex, + receivedChunks: session.chunks.size, + expectedChunks: session.expectedChunks, + percent: session.progress().percent, + }); + } catch (err) { + console.error(`[HTTP] Chunk ${chunkIndex} error for ${req.params.id.slice(0, 8)}:`, err.message); + res.status(500).json({ error: err.message }); + } +}); + // Get session progress app.get("/session/:id", (req, res) => { const session = sessions.get(req.params.id);