feat: AMPP folder sync integration — pre-create folder hierarchy on upload, expose lookup endpoint for Script Task: upload.js

This commit is contained in:
Zac Gaetano 2026-04-18 13:42:09 -04:00
parent e25e63b3f0
commit 0e36ca9972

View file

@ -10,6 +10,7 @@ import {
CompleteMultipartUploadCommand,
AbortMultipartUploadCommand,
} from '@aws-sdk/client-s3';
import { getAmppConfig, ensureFolderPath } from '../ampp/client.js';
const router = express.Router();
@ -30,6 +31,73 @@ const thumbnailQueue = new Queue('thumbnail', {
},
});
// ---------------------------------------------------------------
// AMPP Sync Helpers
// ---------------------------------------------------------------
/**
* Walk up the bins table to build an ordered array of folder name segments
* from the root ancestor down to the given binId.
*/
async function resolveBinPath(binId) {
const segments = [];
let currentId = binId;
while (currentId) {
const result = await pool.query(
'SELECT id, name, parent_id FROM bins WHERE id = $1',
[currentId]
);
if (result.rows.length === 0) break;
const bin = result.rows[0];
segments.unshift(bin.name); // prepend → final order is root-to-leaf
currentId = bin.parent_id;
}
return segments;
}
/**
* Fire-and-forget: ensure the AMPP folder hierarchy exists for this asset's
* project/bin, then persist the resulting folder:id on the asset record so
* the AMPP Script Task can look it up and do the final link.
*
* Never throws logs failures but does NOT fail the Dragon-Wind upload.
*/
async function syncToAmpp(assetId, projectId, binId) {
try {
const config = await getAmppConfig();
if (!config) return; // AMPP not configured — skip silently
// Look up project name
const projResult = await pool.query(
'SELECT name FROM projects WHERE id = $1',
[projectId]
);
if (projResult.rows.length === 0) return;
const projectName = projResult.rows[0].name;
// Build folder path: ProjectName / [BinAncestors...] / BinName
const segments = [projectName];
if (binId) {
const binSegments = await resolveBinPath(binId);
segments.push(...binSegments);
}
// Create or verify folder hierarchy in AMPP
const folderId = await ensureFolderPath(config, segments);
if (!folderId) return;
// Persist AMPP folder ID on asset so Script Task can look it up by filename
await pool.query(
'UPDATE assets SET ampp_folder_id = $1, ampp_synced_at = NOW() WHERE id = $2',
[folderId, assetId]
);
console.log(`[AMPP] asset ${assetId} → folder ${folderId} (${segments.join(' / ')})`);
} catch (err) {
console.error(`[AMPP] sync failed for asset ${assetId}:`, err.message);
}
}
// POST /api/v1/upload/init - Initialize a multipart upload
router.post('/init', async (req, res, next) => {
try {
@ -164,20 +232,22 @@ router.post('/complete', async (req, res, next) => {
const asset = result.rows[0];
// Queue proxy generation job
// Queue proxy and thumbnail generation
await proxyQueue.add('generate', {
assetId,
inputKey: key,
outputKey: `proxies/${assetId}.mp4`,
});
// Queue thumbnail generation job
await thumbnailQueue.add('generate', {
assetId,
inputKey: `proxies/${assetId}.mp4`,
outputKey: `thumbnails/${assetId}.jpg`,
});
// Sync AMPP folder structure — non-blocking, never fails the upload
syncToAmpp(asset.id, asset.project_id, asset.bin_id);
res.json(asset);
} catch (err) {
next(err);
@ -266,20 +336,22 @@ router.post('/simple', upload.single('file'), async (req, res, next) => {
const result = await pool.query(updateQuery, [assetId]);
const asset = result.rows[0];
// Queue proxy generation job
// Queue proxy and thumbnail generation
await proxyQueue.add('generate', {
assetId,
inputKey: s3Key,
outputKey: `proxies/${assetId}.mp4`,
});
// Queue thumbnail generation job
await thumbnailQueue.add('generate', {
assetId,
inputKey: `proxies/${assetId}.mp4`,
outputKey: `thumbnails/${assetId}.jpg`,
});
// Sync AMPP folder structure — non-blocking, never fails the upload
syncToAmpp(assetId, projectId, binId || null);
res.json(asset);
} catch (err) {
next(err);