From 3154cce37c029b1aa40a25e977c2345200cebe8d Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sat, 16 May 2026 18:56:38 -0400 Subject: [PATCH] fix: ETag case mismatch in multipart upload complete route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api.js sends parts as { partNumber, ETag } (uppercase) but upload.js was reading p.etag (lowercase), resulting in undefined ETag passed to S3 CompleteMultipartUpload → InvalidPart error on all large file uploads. Also handle both casings defensively. --- services/mam-api/src/routes/upload.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/services/mam-api/src/routes/upload.js b/services/mam-api/src/routes/upload.js index 528d7e2..ae1d46f 100644 --- a/services/mam-api/src/routes/upload.js +++ b/services/mam-api/src/routes/upload.js @@ -83,6 +83,14 @@ async function syncToAmpp(assetId, projectId, binId) { } } +// Derive a media_type string from a MIME type +function mediaTypeFromMime(mime = '') { + if (mime.startsWith('video')) return 'video'; + if (mime.startsWith('audio')) return 'audio'; + if (mime.startsWith('image')) return 'image'; + return 'document'; +} + // POST /api/v1/upload/init - Initialize a multipart upload router.post('/init', async (req, res, next) => { try { @@ -106,9 +114,7 @@ router.post('/init', async (req, res, next) => { VALUES ($1,$2,$3,$4,$4,'ingesting',$5,$6,$7,$8,NOW(),NOW())`, [ assetId, projectId, binId || null, filename, - contentType.startsWith('video') ? 'video' - : contentType.startsWith('audio') ? 'audio' - : contentType.startsWith('image') ? 'image' : 'document', + mediaTypeFromMime(contentType), s3Key, fileSize, tagsArray.length > 0 ? tagsArray : null, ] @@ -179,7 +185,11 @@ router.post('/complete', async (req, res, next) => { Key: key, UploadId: uploadId, MultipartUpload: { - Parts: parts.map(p => ({ ETag: p.etag, PartNumber: p.partNumber })), + // Accept both casings: api.js sends ETag (uppercase), defend against both + Parts: parts.map(p => ({ + ETag: p.ETag || p.etag, + PartNumber: p.partNumber || p.PartNumber, + })), }, }) ); @@ -261,9 +271,7 @@ router.post('/simple', upload.single('file'), async (req, res, next) => { VALUES ($1,$2,$3,$4,$4,'ingesting',$5,$6,$7,$8,NOW(),NOW())`, [ assetId, projectId, binId || null, filename, - mimeType.startsWith('video') ? 'video' - : mimeType.startsWith('audio') ? 'audio' - : mimeType.startsWith('image') ? 'image' : 'document', + mediaTypeFromMime(mimeType), s3Key, req.file.size, tagsArray.length > 0 ? tagsArray : null, ]