fix: ETag case mismatch in multipart upload complete route

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.
This commit is contained in:
Zac Gaetano 2026-05-16 18:56:38 -04:00
parent 17646c1155
commit 3154cce37c

View file

@ -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 // POST /api/v1/upload/init - Initialize a multipart upload
router.post('/init', async (req, res, next) => { router.post('/init', async (req, res, next) => {
try { 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())`, VALUES ($1,$2,$3,$4,$4,'ingesting',$5,$6,$7,$8,NOW(),NOW())`,
[ [
assetId, projectId, binId || null, filename, assetId, projectId, binId || null, filename,
contentType.startsWith('video') ? 'video' mediaTypeFromMime(contentType),
: contentType.startsWith('audio') ? 'audio'
: contentType.startsWith('image') ? 'image' : 'document',
s3Key, fileSize, s3Key, fileSize,
tagsArray.length > 0 ? tagsArray : null, tagsArray.length > 0 ? tagsArray : null,
] ]
@ -179,7 +185,11 @@ router.post('/complete', async (req, res, next) => {
Key: key, Key: key,
UploadId: uploadId, UploadId: uploadId,
MultipartUpload: { 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())`, VALUES ($1,$2,$3,$4,$4,'ingesting',$5,$6,$7,$8,NOW(),NOW())`,
[ [
assetId, projectId, binId || null, filename, assetId, projectId, binId || null, filename,
mimeType.startsWith('video') ? 'video' mediaTypeFromMime(mimeType),
: mimeType.startsWith('audio') ? 'audio'
: mimeType.startsWith('image') ? 'image' : 'document',
s3Key, req.file.size, s3Key, req.file.size,
tagsArray.length > 0 ? tagsArray : null, tagsArray.length > 0 ? tagsArray : null,
] ]