feat: AMPP placement worker — background job poller, folder list API, pending placements
This commit is contained in:
parent
5fe3df60b5
commit
34695f3caf
1 changed files with 51 additions and 4 deletions
55
server.js
55
server.js
|
|
@ -11,7 +11,8 @@ const fs = require("fs");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const archiver = require("archiver");
|
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;
|
require("events").defaultMaxListeners = 50;
|
||||||
|
|
||||||
|
|
@ -78,6 +79,7 @@ function loadData() {
|
||||||
// Migrate existing data to include new fields if missing
|
// Migrate existing data to include new fields if missing
|
||||||
function migrateData(data) {
|
function migrateData(data) {
|
||||||
if (!data.shareLinks) data.shareLinks = [];
|
if (!data.shareLinks) data.shareLinks = [];
|
||||||
|
if (!data.pendingPlacements) data.pendingPlacements = [];
|
||||||
for (const u of data.users) {
|
for (const u of data.users) {
|
||||||
if (u.quotaMB === undefined) u.quotaMB = 0;
|
if (u.quotaMB === undefined) u.quotaMB = 0;
|
||||||
if (!u.allowedFolders) u.allowedFolders = [];
|
if (!u.allowedFolders) u.allowedFolders = [];
|
||||||
|
|
@ -138,6 +140,20 @@ function initS3() {
|
||||||
|
|
||||||
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
|
// Upload with timeout
|
||||||
function withTimeout(promise, ms, label) {
|
function withTimeout(promise, ms, label) {
|
||||||
return Promise.race([
|
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.
|
// Server only generates the signed URL and tracks quota/permissions.
|
||||||
app.post("/api/presigned", requireAuth, async (req, res) => {
|
app.post("/api/presigned", requireAuth, async (req, res) => {
|
||||||
if (!s3Client) return res.status(503).json({ success: false, error: "S3 not configured" });
|
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 (!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}` });
|
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) => {
|
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);
|
const user = db.users.find(u => u.username === req.sessionData.user);
|
||||||
if (user && size) {
|
if (user && size) {
|
||||||
user.uploadedBytes = (user.uploadedBytes || 0) + size;
|
user.uploadedBytes = (user.uploadedBytes || 0) + size;
|
||||||
saveData(db);
|
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}`);
|
console.log(`[presigned] Completed: ${key} (${size} bytes) by ${req.sessionData.user}`);
|
||||||
res.json({ success: true });
|
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 }); }
|
} 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 ----
|
// ---- AMPP Folder Placement ----
|
||||||
// POST /api/ampp/place — place an already-ingested AMPP asset into virtual folders.
|
// POST /api/ampp/place — place an already-ingested AMPP asset into virtual folders.
|
||||||
// Body: { assetId: "...", assetName: "NEWS--PKG--clip.mxf" }
|
// Body: { assetId: "...", assetName: "NEWS--PKG--clip.mxf" }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue