From 34695f3caff911f26a815db3134c46d6956ba771 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 30 Apr 2026 17:45:21 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20AMPP=20placement=20worker=20=E2=80=94?= =?UTF-8?q?=20background=20job=20poller,=20folder=20list=20API,=20pending?= =?UTF-8?q?=20placements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index b504f1e..9d0c816 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,8 @@ const fs = require("fs"); const https = require("https"); const http = require("http"); const archiver = require("archiver"); -const { placeAsset } = require("./lib/ampp-folder-placer"); +const { placeAsset, placeAssetInFolderById, listAmppFolders } = require("./lib/ampp-folder-placer"); +const AmppPlacementWorker = require("./lib/ampp-placement-worker"); require("events").defaultMaxListeners = 50; @@ -78,6 +79,7 @@ function loadData() { // Migrate existing data to include new fields if missing function migrateData(data) { if (!data.shareLinks) data.shareLinks = []; + if (!data.pendingPlacements) data.pendingPlacements = []; for (const u of data.users) { if (u.quotaMB === undefined) u.quotaMB = 0; if (!u.allowedFolders) u.allowedFolders = []; @@ -138,6 +140,20 @@ function initS3() { initS3(); +// ==================== AMPP PLACEMENT WORKER ==================== +const placementWorker = new AmppPlacementWorker({ + getAmppBase: getAmppBase, + getAmppToken: getAmppToken, + db, + saveData, +}); +// Worker starts after first AMPP config check (deferred to avoid startup errors +// when AMPP is not yet configured). +function maybeStartWorker() { + if (getAmppApiKey() && !placementWorker._timer) placementWorker.start(30_000); +} +setTimeout(maybeStartWorker, 5000); // give server time to fully init + // Upload with timeout function withTimeout(promise, ms, label) { return Promise.race([ @@ -573,7 +589,7 @@ app.post("/api/upload", requireAuth, upload.array("files", 50), async (req, res) // Server only generates the signed URL and tracks quota/permissions. app.post("/api/presigned", requireAuth, async (req, res) => { if (!s3Client) return res.status(503).json({ success: false, error: "S3 not configured" }); - const { filename, prefix, contentType, size } = req.body; + const { filename, prefix, contentType, size, amppFolderId, amppFolderName } = req.body; if (!filename) return res.status(400).json({ success: false, error: "filename required" }); if (isBlockedFile(filename)) return res.status(400).json({ success: false, error: `Blocked file type: ${filename}` }); @@ -600,14 +616,26 @@ app.post("/api/presigned", requireAuth, async (req, res) => { } }); -// Called by client after a successful direct-to-S3 upload to update quota tracking +// Called by client after a successful direct-to-S3 upload to update quota + record AMPP placement app.post("/api/presigned/complete", requireAuth, (req, res) => { - const { key, size } = req.body; + const { key, size, amppFolderId, amppFolderName, filename } = req.body; const user = db.users.find(u => u.username === req.sessionData.user); if (user && size) { user.uploadedBytes = (user.uploadedBytes || 0) + size; saveData(db); } + // Record AMPP placement if a folder was selected + if (amppFolderId && filename) { + if (!db.pendingPlacements) db.pendingPlacements = []; + db.pendingPlacements.push({ + id: require("crypto").randomBytes(8).toString("hex"), + filename, s3Key: key, amppFolderId, amppFolderName: amppFolderName || amppFolderId, + status: "waiting", createdAt: new Date().toISOString(), + }); + saveData(db); + maybeStartWorker(); + console.log(`[presigned] Queued AMPP placement: ${filename} → folder ${amppFolderName}`); + } console.log(`[presigned] Completed: ${key} (${size} bytes) by ${req.sessionData.user}`); res.json({ success: true }); }); @@ -809,6 +837,25 @@ app.get("/api/ampp/jobs/:jobId", requireAuth, async (req, res) => { } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); +// ---- AMPP Folder List (for UI picker) ---- +app.get("/api/ampp/folders/list", requireAuth, async (req, res) => { + if (!getAmppApiKey()) return res.status(503).json({ success: false, error: "AMPP API key not configured" }); + try { + const token = await getAmppToken(); + const result = await listAmppFolders(getAmppBase(), token); + res.json({ success: true, ...result }); + } catch (err) { + console.error("[AMPP folders] List error:", err.message); + res.status(500).json({ success: false, error: err.message }); + } +}); + +// ---- Pending Placements Status ---- +app.get("/api/ampp/placements", requireAuth, (req, res) => { + const placements = (db.pendingPlacements || []); + res.json({ success: true, placements }); +}); + // ---- AMPP Folder Placement ---- // POST /api/ampp/place — place an already-ingested AMPP asset into virtual folders. // Body: { assetId: "...", assetName: "NEWS--PKG--clip.mxf" }